v3.25.3: the card's BIP commands during test runs

The runner's three FETCH/TR sites (the proactive-drain action, the
expectation, the cleanup drain) built every TERMINAL RESPONSE from the
script, so a card-announced OPEN CHANNEL during a run was answered
"performed successfully" without opening a channel - SEND DATA was
dropped and RECEIVE DATA returned nothing (silent data loss, broken
applet state).

_test_step_tr now routes 0x40-0x44 through _handle_bip_command: the
terminal answers for real, so the card's channels work inside a run.
The script keeps an escape hatch - a respond with `raw` TLVs or a
non-ok result wins (the applet error-path tests).

The BIP events flow during a run again for the channels the card
itself opened: _bip_data_available and _bip_flush_channel_events no
longer stop at the run guard (a channel only exists because the card
opened it) and the runner flushes the link events after each TR, so an
applet's own HTTP/BIP conversation completes inside a scripted run.
BIP control stays blocked (409).

Tests: the drain/expect/cleanup routing, the raw and non-ok-result
overrides, the run-time data-available and link events.  883 frontend
+ 873 Python; sw cache simple-v427.
This commit is contained in:
2026-10-11 14:02:29 +03:00
parent 4a8f855cfc
commit 8d39904d2c
9 changed files with 173 additions and 20 deletions
+23
View File
@@ -525,6 +525,29 @@ The `proactive-drain` action's `respond`/`first` use the same shape (the command
type varies per drained command, so `text`/`item_id` apply where the command
carries them).
### BIP commands in scripts
The card's own BIP commands (`OPEN CHANNEL` 0x40, `CLOSE CHANNEL` 0x41,
`RECEIVE DATA` 0x42, `SEND DATA` 0x43, `GET CHANNEL STATUS` 0x44) are answered
by the **terminal**, not by the script: during a run the channels really open,
the data really flows to/from the configured BIP target, and the BIP events
(Data available, Channel status) flow for the channels the card itself opened -
so an applet's own HTTP/BIP conversation completes inside a scripted run. The
BIP **control** API stays blocked while a run is active (409), like the
terminal's other automatic actions (polling, timers, menu).
A step's `respond` overrides the terminal when it carries `raw` TLVs or a
**non-ok result** - the way to script an applet error path:
```json
{"type": "expect", "command": "OPEN CHANNEL",
"respond": {"result": "refused"}}
```
A plain `{"result": "ok"}` (or no `respond`) leaves the BIP answer to the
terminal; the step log names it (`BIP: the terminal answered OPEN CHANNEL
(0x40)`).
## The two PoR transports (SPI2)
1. **Inline** - the card answers the ENVELOPE `9000` with the PoR in the
+1 -1
View File
@@ -565,7 +565,7 @@
<section class="mb-10">
<h3 id="test-script" class="text-lg font-medium mb-2">8.9 Тест-скрипт</h3>
<p class="text-sm mb-2"><strong>Тест-скрипт</strong> &mdash; упорядоченный список <strong>действий</strong> и <strong>ожиданий</strong> проактивных команд, выполняемый на подключённой карте на стороне сервера. Пока запуск активен, карта принадлежит скрипту: остальные операции с картой отклоняются (HTTP&nbsp;409), фоновый опрос STATUS и автоматическая обработка проактивных команд приостанавливаются &mdash; объявленная картой команда остаётся ожидающей для скрипта, и оба механизма возобновляются по завершении запуска; отчёт о запуске хранится в памяти (в том числе после перезагрузки страницы) до нажатия <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>), поэтому все браузеры работают с одним набором; изменения записываются в хранилище по мере правки (кнопки &laquo;Сохранить&raquo; нет &mdash; черновик без валидного шага остаётся локальным); экспорт/импорт JSON остаётся для резервных копий и переноса скриптов между стендами (старый экспорт из localStorage импортируется как есть).</p>
<p class="text-sm mb-2"><strong>Тест-скрипт</strong> &mdash; упорядоченный список <strong>действий</strong> и <strong>ожиданий</strong> проактивных команд, выполняемый на подключённой карте на стороне сервера. Пока запуск активен, карта принадлежит скрипту: остальные операции с картой отклоняются (HTTP&nbsp;409), фоновый опрос STATUS и автоматическая обработка проактивных команд приостанавливаются &mdash; объявленная картой команда остаётся ожидающей для скрипта, и оба механизма возобновляются по завершении запуска. Собственные BIP-команды карты (OPEN/SEND/RECEIVE/CLOSE CHANNEL, GET CHANNEL STATUS) — исключение: терминал отвечает на них по-настоящему: каналы продолжают работать во время запуска, а BIP-события приходят для каналов, открытых самой картой (управление BIP остаётся заблокированным) — если только <code class="font-mono text-sm">respond</code> шага не переопределяет ответ через <code class="font-mono text-sm">raw</code> TLV или результат, отличный от ok; отчёт о запуске хранится в памяти (в том числе после перезагрузки страницы) до нажатия <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>), поэтому все браузеры работают с одним набором; изменения записываются в хранилище по мере правки (кнопки &laquo;Сохранить&raquo; нет &mdash; черновик без валидного шага остаётся локальным); экспорт/импорт JSON остаётся для резервных копий и переноса скриптов между стендами (старый экспорт из localStorage импортируется как есть).</p>
<p class="text-sm mb-2">У вкладки <strong>четыре представления</strong>: <strong>список наборов</strong> (по умолчанию; <em>Добавить набор</em> / <em>Редактировать</em> / <em>Запустить</em> / <em>Экспорт</em> / <em>Удалить</em>), <strong>редактор набора</strong> (скрипты набора в порядке выполнения &mdash; необязательный <em>setup</em> первым и <em>teardown</em> последним, с пометками ролей; у каждого скрипта <em>Запустить</em>/<em>Редактировать</em>/<em>Переместить</em>/<em>Скопировать</em>/<em>Удалить</em>, политика <em>при ошибке</em> <em>остановить</em> (по умолчанию) / <em>продолжить</em> и <em>Требуется ADM</em>), <strong>редактор скрипта</strong> (список шагов; кнопка <em>Старт</em> выполняет только этот скрипт и показывает результат под шагами &mdash; он не сохраняется) и <strong>отчёт набора</strong> (после запуска набора: статус каждого скрипта, таблица шагов с ожидаемым/фактическим, примечание об ошибке и выдержка из журнала запуска; сводка с успешно/предупреждения/ошибки/пропущено, время выполнения, счётчики SCP80 до&nbsp;&rarr;&nbsp;после по каждому keyset, пресет и предупреждение о смене сессии карты; экспорт в <em>JSON</em> или <em>Markdown</em>). <strong>Запуск набора</strong> выполняет его скрипты по порядку в <strong>одной сессии карты, без сбросов и без продолжения с места остановки</strong>; <em>при ошибке</em> решает, остановится ли набор на этом скрипте или запустится следующий, <strong>teardown выполняется всегда</strong> при остановке или ошибке (пока карта в ридере), чтобы следующий тест начался с чистого состояния, а сброс карты останавливает набор и отмечается в отчёте. <strong>Начать со сброса карты</strong> сбрасывает карту и выполняет полную переинициализацию перед запуском набора (скрипты затем работают со свежеинициализированной картой); пресет должен по-прежнему совпадать с ICCID карты, а требуемый ADM проверяется заново для новой сессии карты. Инициализация поглощает начальные proactive-команды карты, поэтому скрипт не поймает первую команду после сброса (они остаются в журнале proactive). <strong>Требуется ADM</strong> (у набора или скрипта) проверяет ключ ADM из подобранного пресета <strong>один раз</strong> перед запуском &mdash; ошибка отклоняет запуск (проверьте вручную через значок ADM; автоматических повторов нет). Скрипты хранятся в хранилище наборов (<code class="font-mono text-sm">~/.pysim-simple-server/test_suites.json</code>, <code class="font-mono text-sm">--test-suites</code>); каждый скрипт принадлежит ровно одному набору, и скрипт можно переместить или скопировать в другой набор (экспорт набора &mdash; это пакет с набором и его скриптами; старый экспорт только скриптов импортируется в автоматически созданный набор &laquo;Imported scripts&raquo;).</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Действия</strong> &mdash; <em>Envelope</em> (Event Download: сырой тип события + данные, с опциональным переопределением источника &mdash; 82 терминал / 83 сеть) или <em>Event</em> (семантические формы: <strong>та же форма, что и в диалоге отправки на вкладке «Телефон»</strong>, для всех смоделированных событий &mdash; четыре события, которые собирает сервер (Location status, Access technology change, Network rejection, Data connection status change), передаются полями, которые он проверяет и собирает во время запуска, остальные несут собранный формой hex данных события (показывается под формой, с проверкой одного ENVELOPE); источник device identities берётся из события (например, MT call &rarr; сеть) или из его поля источника), <em>Menu Selection</em> (item id 1&ndash;255 или текст пункта с точным/частичным совпадением, по умолчанию без учёта регистра &mdash; выбор <em>«Учёт регистра»</em> в редакторе шагов включает точное совпадение; текст подбирается по меню карты, которое следует за параметрами установки апплета), <em>UPDATE/READ файла</em> (путь + режим; чтение проверяет данные файла), <em>сырой APDU</em> (без авто-обработчика), <em>SCP80</em> (C-APDU или готовый защищённый пакет, опциональные переопределения номера keyset (kvn) / TAR / SPI1 / SPI2 и формат команды &mdash; компактный или обёрнутый в шаблон Command Scripting TS 102 226 &sect;5.2.1 <em>expanded</em> (AA) / <em>expanded AE&nbsp;80</em>; ключи и счётчик берутся из пресета карты, подобранного по ICCID подключённой карты &mdash; запуск называет пресет в хранилище, поэтому всегда используются его keysets, TAR и счётчик &mdash; а сервер сохраняет потреблённый счётчик в пресет по ходу запуска), <em>STATUS</em> (при <em>попытках</em> &gt; 1 опрашивает, пока карта не объявит команду) и <em>Очистка очереди проактивных команд</em> (опрос STATUS и поглощение всех объявленных команд &mdash; результат TERMINAL RESPONSE для каждой, опционально другой результат для первой; граница попыток/интервала; опциональная проверка <em>require</em> по поглощённому. Пустой опрос проходит, финальный STATUS должен быть idle, а оставшаяся команда передаётся следующему шагу &mdash; используйте в начале, середине или конце скрипта, чтобы убедиться, что карта перестала слать команды; она же поглощает команду, оставшуюся от предыдущего шага (ответившего <code class="font-mono text-sm">91XX</code>)). У каждого действия есть проверка ответа: ожидаемые SW и данные (точно или маска с подстановкой <code class="font-mono text-sm">?</code> &mdash; <code class="font-mono text-sm">91??</code>) и проверка PoR для SCP80 (<em>любой / без PoR / por_ok</em> или своя проверка декодированного PoR &mdash; статус / SW / данные; транспорт через SEND SHORT MESSAGE работает так же).</li>
+1 -1
View File
@@ -564,7 +564,7 @@
<section class="mb-10">
<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&nbsp;409), and background STATUS polling and the automatic proactive handling are suspended &mdash; a command the card announces stays pending for the script, and both resume when the run ends; 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 &mdash; 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>
<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&nbsp;409), and background STATUS polling and the automatic proactive handling are suspended &mdash; a command the card announces stays pending for the script, and both resume when the run ends. The card&rsquo;s own BIP commands (OPEN/SEND/RECEIVE/CLOSE CHANNEL, GET CHANNEL STATUS) are an exception: the terminal answers them for real &mdash; its channels keep working during the run and the BIP events flow for the channels the card itself opened (BIP <em>control</em> stays blocked) &mdash; unless the step&rsquo;s <code class="font-mono text-sm">respond</code> overrides the answer with <code class="font-mono text-sm">raw</code> TLVs or a non-ok result; 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 &mdash; 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>
<p class="text-sm mb-2">The pill has <strong>four views</strong>: the <strong>suite list</strong> (default; <em>Add suite</em> / <em>Edit</em> / <em>Run</em> / <em>Export</em> / <em>Delete</em>), the <strong>suite editor</strong> (the suite's scripts in run order &mdash; the optional <em>setup</em> first and the <em>teardown</em> last, marked with role badges; per script <em>Run</em>/<em>Edit</em>/<em>Move</em>/<em>Copy</em>/<em>Delete</em>, the per-member <em>on failure</em> policy <em>stop</em> (default) / <em>continue</em>, and <em>ADM required</em>), the <strong>script editor</strong> (the step list; its <em>Start</em> runs that single script and shows the result under the steps &mdash; not preserved) and the <strong>suite report</strong> (after a suite run: every script's status, its step table with expected/actual, the failing note and a run-log excerpt; the summary with passed/warnings/failed/skipped, wall time, the SCP80 counters before&nbsp;&rarr;&nbsp;after per keyset, the preset and a card-session-changed warning; export as <em>JSON</em> or <em>Markdown</em>). A <strong>suite run</strong> executes its scripts in order in <strong>one card session, without resets and without resume</strong>; <em>on failure</em> decides whether the suite stops at that script or runs the next one, the <strong>teardown always runs</strong> on a stop or a failure (while the card is in the reader) so the next test starts clean, and a card reset stops the suite and is flagged in the report. <strong>Start with card reset</strong> resets the card and runs the full re-initialization before the suite starts (the scripts then run against a freshly initialized card); the preset must still match the card's ICCID afterwards and a required ADM is verified again for the new card session. The initialization consumes the card's initial proactive commands, so a script cannot catch the first command after the reset (they stay in the proactive log). <strong>ADM required</strong> (on the suite or a script) verifies the matched preset's ADM key <strong>once</strong> before the start &mdash; a failure refuses the run (verify manually with the ADM badge; nothing is retried automatically). Scripts live in the suite's store (<code class="font-mono text-sm">~/.pysim-simple-server/test_suites.json</code>, <code class="font-mono text-sm">--test-suites</code>); every script belongs to exactly one suite, and a script can be moved or copied to another suite (a bundle export carries the suite plus its scripts; the old script-only export imports into an auto-created &ldquo;Imported scripts&rdquo; suite).</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Actions</strong> &mdash; <em>Envelope</em> (Event Download: raw event type + data, with an optional source override &mdash; 82 terminal / 83 network) or <em>Event</em> (the semantic forms: the <strong>same form the Phone tab's send dialog uses</strong>, for every modelled event &mdash; the four events the server builds (Location status, Access technology change, Network rejection, Data connection status change) travel as fields it validates and builds at run time, every other event carries the form's built event-data hex (shown live under the form, with the one-ENVELOPE check); the device-identities source comes from the event (e.g. MT call &rarr; network) or its source field), <em>Menu Selection</em> (item id 1&ndash;255, or the item's text with an exact/contains match, case-insensitive by default &mdash; the step editor's <em>Match case</em> choice switches to an exact-case 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 &mdash; compact, or the TS 102 226 &sect;5.2.1 Command Scripting template <em>expanded</em> (AA) / <em>expanded AE&nbsp;80</em> around the C-APDU; the keys and the counter come from the card preset matched by the equipped card's ICCID &mdash; the run names the stored preset, so it always uses its keysets, TARs and counter &mdash; the server persists the consumed counter into it as the run proceeds), <em>STATUS</em> (with <em>attempts</em> &gt; 1 it polls until the card announces a command) and <em>Proactive drain</em> (poll STATUS and consume every announced command &mdash; the TERMINAL RESPONSE result for each, optionally a different result for the first; the attempt/interval bound; an optional <em>require</em> assertion over what was drained. An empty poll passes, the final STATUS must be idle, and a still-pending command is handed to the next step &mdash; use it at the start, in the middle or at the end of a script to make sure the card has stopped sending; it also consumes a command a previous step left pending (a step that answered <code class="font-mono text-sm">91XX</code>). Every action has a response check: expected SW and data (exact, or a mask with <code class="font-mono text-sm">?</code> nibble wildcards &mdash; <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 &mdash; status / SW / data; the SEND SHORT MESSAGE transport works the same).</li>
+1 -1
View File
@@ -1905,7 +1905,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.25.2';
const SIMPLE_VERSION = '3.25.3';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== view history (the browser Back/Forward buttons) =====
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v426';
const CACHE = 'simple-v427';
const URLS = [
'index.html',
'help.html',
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
version = "3.25.2"
version = "3.25.3"
description = "HTTP REST server wrapping pysim for the SIMple PWA"
requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
+39 -13
View File
@@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.25.2'
VERSION = '3.25.3'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -3557,10 +3557,11 @@ def _bip_data_available(ch):
server = _server_ref
if not server or not _CARD_CONNECTED or not getattr(server, 'scc', None):
return False
if _PROACTIVE_BUSY or getattr(server, 'stk_pending', None) or _TEST_RUNNING:
# a test run owns the card: the bytes stay un-notified and the
# monitor retries on its next tick (v3.25.1)
if _PROACTIVE_BUSY or getattr(server, 'stk_pending', None):
return False
# A test run owns the card, but this event is only ever sent for a
# channel the card itself opened - the card's own BIP conversation
# continuing - so it flows during a run too (v3.25.3).
length = min(len(ch.rx), 0xFF)
tlv = bytes([0xB8, 0x02, 0x80 | (ch.id & 0x07), 0x00,
0xB7, 0x01, length])
@@ -5116,9 +5117,9 @@ def _bip_flush_channel_events(scc):
global _FLUSHING_CHANNEL_EVENTS
if _FLUSHING_CHANNEL_EVENTS:
return
if _TEST_RUNNING:
# a test run owns the card: the events stay queued (v3.25.1)
return
# The queued events describe channels the card itself opened: they flow
# during a test run too - the card must learn that its link died, else it
# waits for the run to end (v3.25.3).
if not _BIP_LINK_EVENTS:
_BIP.take_pending_events()
return
@@ -6613,6 +6614,28 @@ def _test_terminal_response(cmd_num, cmd_type, dev_dst, dev_src, cmd_qual, respo
cd_tag=cd_tag)
_BIP_CMD_TYPES = (0x40, 0x41, 0x42, 0x43, 0x44)
def _test_step_tr(scc, cmd_num, cmd_type, dev_src, dev_dst, cmd_qual, cd_tag,
respond, raw):
"""The TERMINAL RESPONSE of a test step for a fetched command: the card's
own BIP commands (0x40-0x44) are handled by the terminal, so its channels
keep working while a run owns the card (v3.25.3). The script overrides
the answer when it carries `raw` TLVs or a non-ok result (an applet error
path to test); everything else goes through the scripted answer."""
r = respond or {}
if cmd_type in _BIP_CMD_TYPES and not r.get('raw') and r.get('result', 0) == 0x00:
tr = _handle_bip_command(scc, cmd_num, cmd_type, cmd_qual, raw,
dev_src, dev_dst, cd_tag=cd_tag)
if tr:
_test_log('BIP: the terminal answered %s (0x%02X)'
% (PROACTIVE_TYPE_NAMES.get(cmd_type, '?'), cmd_type))
return tr
return _test_terminal_response(cmd_num, cmd_type, dev_dst, dev_src,
cmd_qual, respond, cd_tag=cd_tag)
def _test_run_drain(server, step):
"""The proactive drain action: poll STATUS and consume every announced
command - the TERMINAL RESPONSE is `first` for the first one, `respond`
@@ -6649,10 +6672,11 @@ def _test_run_drain(server, step):
if menu:
server.sim_menu = menu
respond = p['first'] if (not consumed and p.get('first')) else p['respond']
tr = _test_terminal_response(cmd_num, cmd_type, dev_dst, dev_src,
cmd_qual, respond, cd_tag=cd_tag)
tr = _test_step_tr(scc, cmd_num, cmd_type, dev_src, dev_dst, cmd_qual,
cd_tag, respond, raw)
tr_rv = scc._tp.send_apdu('%s140000%02x%s' % (scc.cat_cla, len(tr), tr.hex()))
_test_log('TR=%s -> SW=%s' % (tr.hex().upper(), tr_rv[1]))
_bip_flush_channel_events(scc)
consumed.append({'type_hex': '%02X' % cmd_type, 'type_name': name,
'qualifier': ('%02X' % cmd_qual) if cmd_qual is not None else None,
'tr_sw': tr_rv[1], 'raw': raw.hex().upper()})
@@ -7204,12 +7228,13 @@ def _test_run_expect(server, step, pending, ctx=None):
checks.append(_test_check_result(
'Files', sorted(paths) == sorted(want), ', '.join(want),
', '.join(paths) or '(none)', c['on_fail']))
tr = _test_terminal_response(cmd_num, cmd_type, dev_dst, dev_src, cmd_qual,
step['respond'], cd_tag=cd_tag)
tr = _test_step_tr(scc, cmd_num, cmd_type, dev_src, dev_dst, cmd_qual,
cd_tag, step['respond'], raw)
tr_rv = scc._tp.send_apdu('%s140000%02x%s' % (scc.cat_cla, len(tr), tr.hex()))
tr_sw = tr_rv[1]
_test_log('TR=%s -> SW=%s' % (tr.hex().upper(), tr_sw))
_record_tr(log_entry, tr, tr_sw)
_bip_flush_channel_events(scc)
status = testscript.combine_levels([c['level'] if not c['ok'] else 'ok' for c in checks])
result = {'status': status, 'sw': tr_sw, 'data': raw.hex().upper(),
'sent': 'TR ' + tr.hex().upper(), 'checks': checks,
@@ -7244,9 +7269,10 @@ def _test_drain_pending(scc, fetch_len, result=0x00, max_commands=3):
break
raw = bytes.fromhex(fdata)
cmd_num, cmd_type, dev_src, dev_dst, cmd_qual, cd_tag = _parse_proactive_header(raw)
tr = _test_terminal_response(cmd_num, cmd_type, dev_dst, dev_src,
cmd_qual, {'result': result}, cd_tag=cd_tag)
tr = _test_step_tr(scc, cmd_num, cmd_type, dev_src, dev_dst,
cmd_qual, cd_tag, {'result': result}, raw)
rv = scc._tp.send_apdu('%s140000%02x%s' % (scc.cat_cla, len(tr), tr.hex()))
_bip_flush_channel_events(scc)
name = PROACTIVE_TYPE_NAMES.get(cmd_type, '?')
answered.append({'name': name, 'type': cmd_type,
'raw': raw.hex().upper(), 'tr': tr.hex().upper(),
+27 -2
View File
@@ -185,11 +185,13 @@ class TestProactiveChainOwnership(unittest.TestCase):
1100-command loop held _CARD_LOCK and blocked a run's start)."""
def setUp(self):
self.saved = (S._TEST_RUNNING, S._server_ref, S._CARD_CONNECTED)
self.saved = (S._TEST_RUNNING, S._server_ref, S._CARD_CONNECTED,
S._BIP_LINK_EVENTS, S._FLUSHING_CHANNEL_EVENTS)
S._TEST_RUNNING = False
def tearDown(self):
S._TEST_RUNNING, S._server_ref, S._CARD_CONNECTED = self.saved
(S._TEST_RUNNING, S._server_ref, S._CARD_CONNECTED,
S._BIP_LINK_EVENTS, S._FLUSHING_CHANNEL_EVENTS) = self.saved
def _fetches(self, scc):
return [a for a in scc.sent if a.lower().startswith('8012')]
@@ -238,6 +240,29 @@ class TestProactiveChainOwnership(unittest.TestCase):
S._sim_resume_poll()
timer.assert_not_called()
def test_data_available_flows_during_a_run(self):
# a BIP channel only exists because the card opened it: the event is
# the card's own conversation continuing, so it flows during a run
# (v3.25.3 - the card must not wait for the run to end)
ch = types.SimpleNamespace(peer_closed=False, rx=b'ABCD', id=1)
S._server_ref = types.SimpleNamespace(scc=object(), event_list=[0x09])
S._CARD_CONNECTED = True
S._TEST_RUNNING = True
with mock.patch.object(S, '_send_event_download') as send:
ok = S._bip_data_available(ch)
self.assertTrue(ok)
send.assert_called_once()
self.assertEqual(send.call_args[0][1], 0x09)
def test_link_events_are_taken_during_a_run(self):
# the queued link events describe the card's own channels: the flush
# no longer stops at the run guard (v3.25.3)
S._TEST_RUNNING = True
S._BIP_LINK_EVENTS = False
with mock.patch.object(S._BIP, 'take_pending_events') as take:
S._bip_flush_channel_events(None)
take.assert_called_once()
def test_every_run_end_resumes_the_poll(self):
# the two worker bodies, their two exception paths and the spawn
# failure all call the helper
+79
View File
@@ -2056,5 +2056,84 @@ class TestAdmGate(unittest.TestCase):
self.assertTrue(ok)
class TestRunnerBipCommands(RunnerTestCase):
"""The runner's steps let the terminal answer the card's own BIP commands
(v3.25.3): a channel the card opens during a run really opens and its data
really flows - unless the script overrides the answer with `raw` TLVs or a
non-ok result (an applet error path to test)."""
OPEN = 'D02B8103014001820281828500B50103B9020200C70B076D656761666F6E2E7275'
BIP_TR = bytes.fromhex('010301400002028281030100')
def _tr(self, scc):
return [a for a in scc.sent if a.startswith('8014')][-1]
def _pending(self, scc, cmd):
scc.push('80F2', '', '9109') # STATUS: the command is pending
scc.push('8012', cmd, '9000')
scc.push('8014', '', '9000')
def _run_bip(self, scc, steps):
with mock.patch.object(S, '_handle_bip_command',
return_value=self.BIP_TR) as bip:
run = self.run_script(FakeServer(scc), steps)
return run, bip
def test_a_drain_lets_the_terminal_answer_bip(self):
scc = FakeScc()
scc.push('80F2', '', '9109').push('8012', self.OPEN, '9000')
scc.push('8014', '', '9000').push('80F2', '', '9000')
run, bip = self._run_bip(scc, [
{'type': 'action', 'kind': 'proactive-drain',
'params': {'attempts': 2, 'respond': {'result': 'ok'}}},
])
self.assertEqual(run['status'], 'ok', run['steps'])
self.assertEqual(bip.call_count, 1)
self.assertEqual(bip.call_args[0][2], 0x40)
self.assertTrue(self._tr(scc).endswith(self.BIP_TR.hex().upper()),
self._tr(scc))
def test_an_expectation_lets_the_terminal_answer_bip(self):
scc = FakeScc()
self._pending(scc, self.OPEN)
run, bip = self._run_bip(scc, [
{'type': 'action', 'kind': 'status', 'params': {'attempts': 2}},
{'type': 'expect', 'command': 'ANY', 'respond': {'result': 'ok'}},
])
self.assertEqual(run['status'], 'ok', run['steps'])
self.assertEqual(bip.call_count, 1)
def test_a_raw_answer_overrides_the_terminal_bip(self):
scc = FakeScc()
self._pending(scc, self.OPEN)
_run, bip = self._run_bip(scc, [
{'type': 'action', 'kind': 'status', 'params': {'attempts': 2}},
{'type': 'expect', 'command': 'ANY',
'respond': {'result': 'ok', 'raw': '010301400002028281030100'}},
])
self.assertEqual(bip.call_count, 0)
self.assertTrue(self._tr(scc).endswith('010301400002028281030100'),
self._tr(scc))
def test_a_non_ok_result_overrides_the_terminal_bip(self):
scc = FakeScc()
self._pending(scc, self.OPEN)
_run, bip = self._run_bip(scc, [
{'type': 'action', 'kind': 'status', 'params': {'attempts': 2}},
{'type': 'expect', 'command': 'ANY', 'respond': {'result': 'refused'}},
])
self.assertEqual(bip.call_count, 0)
self.assertIn('83022200', self._tr(scc))
def test_the_cleanup_drain_handles_bip(self):
scc = FakeScc()
scc.push('8012', self.OPEN, '9000')
with mock.patch.object(S, '_handle_bip_command',
return_value=self.BIP_TR) as bip:
drained = S._test_drain_pending(scc, 9)
self.assertEqual(bip.call_count, 1)
self.assertIn('OPEN CHANNEL', drained.get('note', ''))
if __name__ == '__main__':
unittest.main()