server: guarantee a TERMINAL RESPONSE for paused proactive commands
Every FETCHed proactive command must be answered, otherwise the card is left in an unfinished session and stops issuing commands (e.g. it will not deliver a PoR for SEND SM). The menu handlers now share _menu_send_response, and a server-side watchdog (_arm_menu_timeout / _menu_timeout_fire, --menu-timeout, default 60s, 0 disables) sends the timeout result (0x12) when the user never answers. The timer is armed while a command is pending and cancelled on any response, equip, rescue and card disconnect. Also fixes docs/api.md, which had back (0x11) and timeout (0x12) codes swapped. Tests for arm/cancel/clamping and the flat timeout TR. SW cache v89 -> v90.
This commit is contained in:
@@ -628,6 +628,7 @@ pysim-otaman-server --http-port 8080
|
||||
| `--terminal-profile` | TERMINAL PROFILE payload hex (default 10-byte GSM profile) |
|
||||
| `--poll-interval` | Idle interval before automatic STATUS polling (default 30s; `0` disables polling) |
|
||||
| `--fast-init` | Init/equip without redundant card resets (only explicit equip/reset resets the card) |
|
||||
| `--menu-timeout` | Auto-answer a paused STK command with a timeout TERMINAL RESPONSE (default 60s; `0` disables) |
|
||||
| `--timing` | Log phase durations, card resets and APDU counters with elapsed timestamps |
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
@@ -608,6 +608,7 @@ pysim-otaman-server --http-port 8080
|
||||
| `--log-requests` | Лог запросов/ответов в stderr |
|
||||
| `--poll-interval` | Интервал автоопроса STATUS (по умолчанию 30с; `0` отключает опрос) |
|
||||
| `--fast-init` | Инициализация/equip без лишних сбросов карты (сброс только по явным equip/reset) |
|
||||
| `--menu-timeout` | Автоответ timeout TERMINAL RESPONSE на приостановленную STK-команду (по умолчанию 60с; `0` отключает) |
|
||||
| `--timing` | Лог длительности фаз, сбросов карты и счётчиков APDU с отметками времени |
|
||||
|
||||
### Устранение неполадок
|
||||
|
||||
+6
-4
@@ -245,7 +245,9 @@ or
|
||||
### `POST /api/menu-respond`
|
||||
|
||||
Sends `TERMINAL RESPONSE` to the current proactive command with the given result
|
||||
code. Continues the proactive chain if the card responds with `91XX`.
|
||||
code. Continues the proactive chain if the card responds with `91XX`. If no
|
||||
response arrives within `--menu-timeout` seconds (default 60, `0` disables), the
|
||||
server watchdog sends the `timeout` result itself.
|
||||
|
||||
```json
|
||||
{"result": "ok", "item_id": 1}
|
||||
@@ -254,9 +256,9 @@ code. Continues the proactive chain if the card responds with `91XX`.
|
||||
| `result` | TERMINAL RESPONSE code | Meaning |
|
||||
|---|---|---|
|
||||
| `ok` | `0x00` | Command performed successfully |
|
||||
| `back` | `0x12` | Backward move requested |
|
||||
| `cancel` | `0x10` | Proactive session terminated |
|
||||
| `timeout` | `0x11` | No response from user |
|
||||
| `cancel` | `0x10` | Proactive session terminated by the user |
|
||||
| `back` | `0x11` | Backward move in the proactive session requested by the user |
|
||||
| `timeout` | `0x12` | No response from the user |
|
||||
|
||||
### `GET /api/stk-status`
|
||||
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
<p class="text-sm mb-3">Работа с сессией Card Application Toolkit: меню STK, подписанные события, журнал проактивных команд, словарь данных PROVIDE LOCAL INFORMATION и опрос STATUS.</p>
|
||||
|
||||
<h4 id="stk-menu" class="font-medium mb-1">5.5.1 Меню STK</h4>
|
||||
<p class="text-sm mb-3">Если карта выдала команду SET UP MENU, вверху этого представления появляется блок «Меню STK» с изумрудной кнопкой <strong>STK: <название></strong>, открывающей оверлей меню (браузер STK-меню карты). Если карта не задала меню, вместо кнопки показывается «Меню не задано картой». Состояние меню обновляется при каждом открытии представления.</p>
|
||||
<p class="text-sm mb-3">Если карта выдала команду SET UP MENU, вверху этого представления появляется блок «Меню STK» с изумрудной кнопкой <strong>STK: <название></strong>, открывающей оверлей меню (браузер STK-меню карты). Если карта не задала меню, вместо кнопки показывается «Меню не задано картой». Состояние меню обновляется при каждом открытии представления. Интерактивные проактивные команды всегда получают TERMINAL RESPONSE: оверлей ждёт вашего выбора, и если вы не ответили и не нажали <strong>Timeout</strong>, сервер сам отвечает результатом timeout через <code class="font-mono text-sm">--menu-timeout</code> секунд (по умолчанию 60, <code class="font-mono text-sm">0</code> отключает).</p>
|
||||
|
||||
<h4 id="subscribed-events" class="font-medium mb-1">5.5.2 Подписанные события (SET UP EVENT LIST)</h4>
|
||||
<p class="text-sm mb-2">События, которые отслеживает карта. У каждого события есть кнопка <strong>Отправить</strong>, открывающая форму, специфичную для типа события:</p>
|
||||
|
||||
+1
-1
@@ -336,7 +336,7 @@
|
||||
<p class="text-sm mb-3">Interacts with the Card Application Toolkit session: the STK menu, subscribed events, the proactive command log, the PROVIDE LOCAL INFORMATION data dictionary, and STATUS polling.</p>
|
||||
|
||||
<h4 id="stk-menu" class="font-medium mb-1">5.5.1 STK menu</h4>
|
||||
<p class="text-sm mb-3">When the card has issued a SET UP MENU command, a “STK menu” block appears at the top of this view with an emerald <strong>STK: <title></strong> button that opens the menu overlay (same as the card’s STK menu browser). If the card has not set up a menu, the block shows “No menu set by the card” instead. The menu state is refreshed each time the view is opened.</p>
|
||||
<p class="text-sm mb-3">When the card has issued a SET UP MENU command, a “STK menu” block appears at the top of this view with an emerald <strong>STK: <title></strong> button that opens the menu overlay (same as the card’s STK menu browser). If the card has not set up a menu, the block shows “No menu set by the card” instead. The menu state is refreshed each time the view is opened. User-interactive proactive commands always get a TERMINAL RESPONSE: the overlay pauses for your choice, and if you neither answer nor press <strong>Timeout</strong>, the server answers with a timeout result after the <code class="font-mono text-sm">--menu-timeout</code> seconds (default 60, <code class="font-mono text-sm">0</code> disables).</p>
|
||||
|
||||
<h4 id="subscribed-events" class="font-medium mb-1">5.5.2 Subscribed events (SET UP EVENT LIST)</h4>
|
||||
<p class="text-sm mb-2">The events the card monitors. Each event has a <strong>Send</strong> button that opens a form specific to the event type:</p>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'otaman-v89';
|
||||
const CACHE = 'otaman-v90';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -12,7 +12,7 @@ from pySim.cards import UiccCardBase
|
||||
|
||||
from .shell import load_pysim_app
|
||||
from . import fastinit
|
||||
from .server import PysimHandler, StderrApduTracer, _LoggingApduTracer, VERSION, _send_terminal_profile, _DefaultProactiveHandler, _handle_proactive_chain, _send_status, _init_proactive_session, _timing_on, _tlog
|
||||
from .server import PysimHandler, StderrApduTracer, _LoggingApduTracer, VERSION, _send_terminal_profile, _DefaultProactiveHandler, _handle_proactive_chain, _send_status, _init_proactive_session, _timing_on, _tlog, _set_menu_timeout
|
||||
|
||||
|
||||
_server_start = 0
|
||||
@@ -52,11 +52,15 @@ def main():
|
||||
help='Log phase durations, card resets and APDU counters with elapsed timestamps')
|
||||
parser.add_argument('--fast-init', action='store_true', default=False,
|
||||
help='Init/equip without redundant card resets (reset only on explicit equip/reset)')
|
||||
parser.add_argument('--menu-timeout', type=int, default=60, metavar='SECS',
|
||||
help='Auto-send a timeout TERMINAL RESPONSE if a paused STK command is not answered (default: 60, 0 disables)')
|
||||
|
||||
opts = parser.parse_args()
|
||||
opts.skip_card_init = opts.no_card_init
|
||||
if opts.timing:
|
||||
_timing_on()
|
||||
if opts.menu_timeout is not None:
|
||||
_set_menu_timeout(opts.menu_timeout)
|
||||
sl = None
|
||||
scc = None
|
||||
card = None
|
||||
|
||||
+124
-86
@@ -688,6 +688,45 @@ def _poll_enable():
|
||||
_POLL_ENABLED = True
|
||||
_reset_poll_timer()
|
||||
|
||||
_MENU_TIMEOUT = 60
|
||||
_MENU_TIMER = None
|
||||
|
||||
def _set_menu_timeout(seconds):
|
||||
global _MENU_TIMEOUT
|
||||
_MENU_TIMEOUT = max(0, min(3600, int(seconds)))
|
||||
|
||||
def _cancel_menu_timeout():
|
||||
global _MENU_TIMER
|
||||
if _MENU_TIMER is not None:
|
||||
_MENU_TIMER.cancel()
|
||||
_MENU_TIMER = None
|
||||
|
||||
def _arm_menu_timeout():
|
||||
"""Watchdog: a paused proactive command must always get a TERMINAL RESPONSE,
|
||||
even if the user never answers. Fires 0x12 ('timeout') via the same path as
|
||||
an explicit user response."""
|
||||
global _MENU_TIMER
|
||||
_cancel_menu_timeout()
|
||||
if _MENU_TIMEOUT <= 0:
|
||||
return
|
||||
_MENU_TIMER = threading.Timer(_MENU_TIMEOUT, _menu_timeout_fire)
|
||||
_MENU_TIMER.daemon = True
|
||||
_MENU_TIMER.start()
|
||||
|
||||
def _menu_timeout_fire():
|
||||
global _MENU_TIMER
|
||||
_MENU_TIMER = None
|
||||
with _CARD_LOCK:
|
||||
server = _server_ref
|
||||
if not server or not getattr(server, 'stk_pending', None) or not getattr(server, 'scc', None):
|
||||
return
|
||||
pd = server.stk_pending
|
||||
sys.stderr.write('MENU-TIMEOUT: auto TR timeout (cmd=%02x type=%02x)\n' % (pd['cmd_num'], pd['cmd_type']))
|
||||
try:
|
||||
_menu_send_response(server, 'timeout', None)
|
||||
except Exception as e:
|
||||
sys.stderr.write('MENU-TIMEOUT error: %s\n' % e)
|
||||
|
||||
def _poll_disable():
|
||||
global _POLL_ENABLED, _POLL_TIMER
|
||||
_POLL_ENABLED = False
|
||||
@@ -983,6 +1022,7 @@ def _record_tr(entry, tr_tlv, tr_sw=None):
|
||||
def _handle_card_disconnect():
|
||||
global _CARD_CONNECTED
|
||||
_poll_disable()
|
||||
_cancel_menu_timeout()
|
||||
_CARD_CONNECTED = False
|
||||
if _server_ref:
|
||||
_server_ref.card = None
|
||||
@@ -1332,6 +1372,79 @@ def _send_terminal_profile(scc, tp_hex):
|
||||
return sim_menu, event_list
|
||||
|
||||
|
||||
def _make_menu_fetch_handler(server, resp):
|
||||
"""on_fetch callback for the menu chain: pauses on user-interactive commands
|
||||
and stores the pending command so a TERMINAL RESPONSE can be sent later."""
|
||||
def _on_menu_fetch(raw, cmd_num, cmd_type, dev_src, dev_dst):
|
||||
if cmd_type == 0x21:
|
||||
text = _parse_display_text(raw) if raw else None
|
||||
if text:
|
||||
server.stk_pending = {'type': 'display_text',
|
||||
'cmd_num': cmd_num, 'cmd_type': cmd_type,
|
||||
'dev_src': dev_src, 'dev_dst': dev_dst, 'text': text}
|
||||
resp.update(type='display_text', text=text)
|
||||
return 'pause'
|
||||
elif cmd_type == 0x24:
|
||||
items = _parse_select_item(raw) if raw else []
|
||||
server.stk_pending = {'type': 'select_item',
|
||||
'cmd_num': cmd_num, 'cmd_type': cmd_type,
|
||||
'dev_src': dev_src, 'dev_dst': dev_dst, 'items': items}
|
||||
resp.update(type='select_item', items=items)
|
||||
return 'pause'
|
||||
elif cmd_type == 0x25:
|
||||
items = _parse_setup_menu_items(raw) if raw else []
|
||||
server.stk_pending = {'type': 'select_item',
|
||||
'cmd_num': cmd_num, 'cmd_type': cmd_type,
|
||||
'dev_src': dev_src, 'dev_dst': dev_dst, 'items': items}
|
||||
resp.update(type='select_item', items=items)
|
||||
return 'pause'
|
||||
return _on_menu_fetch
|
||||
|
||||
|
||||
def _menu_send_response(server, result, item_id=None):
|
||||
"""Send the pending command's TERMINAL RESPONSE and continue the chain.
|
||||
Shared by /api/menu-respond and the user-input timeout watchdog. Returns
|
||||
(payload, http_status)."""
|
||||
if not server.stk_pending:
|
||||
return {'error': 'no pending command'}, 400
|
||||
scc = server.scc
|
||||
RESULT_MAP = {'ok': 0x00, 'cancel': 0x10, 'back': 0x11, 'timeout': 0x12}
|
||||
gr = RESULT_MAP.get(result, 0x00)
|
||||
pd = server.stk_pending
|
||||
cd = bytes([0x81, 0x03, pd['cmd_num'], pd['cmd_type'], 0x00])
|
||||
di = bytes([0x82, 0x02, pd['dev_dst'], pd['dev_src']])
|
||||
tr_data = cd + di
|
||||
if isinstance(item_id, int) and result == 'ok' and pd['type'] == 'select_item':
|
||||
tr_data += bytes([0x90, 0x01, item_id])
|
||||
tr_data += bytes([0x83, 0x02, gr, 0x00])
|
||||
tr_hex = '%s140000%02x%s' % (scc.cat_cla, len(tr_data), tr_data.hex())
|
||||
tr_rv = scc._tp.send_apdu(tr_hex)
|
||||
sys.stderr.write('TR(menu): cmd=%02x type=%02x result=%02x -> %s\n' % (pd['cmd_num'], pd['cmd_type'], gr, tr_rv[1]))
|
||||
for entry in reversed(_PROACTIVE_LOG):
|
||||
if (entry.get('cmd_num') == pd['cmd_num']
|
||||
and entry.get('type_hex') == '%02x' % pd['cmd_type']
|
||||
and 'tr_hex' not in entry):
|
||||
_record_tr(entry, tr_data, tr_rv[1])
|
||||
break
|
||||
sw = tr_rv[1]
|
||||
resp = {'sw': sw}
|
||||
if result == 'cancel':
|
||||
server.stk_pending = None
|
||||
server.menu_active = False
|
||||
else:
|
||||
server.stk_pending = None
|
||||
if sw.startswith('91'):
|
||||
_handle_proactive_chain(scc, sw, _make_menu_fetch_handler(server, resp))
|
||||
else:
|
||||
server.menu_active = False
|
||||
resp['type'] = 'done'
|
||||
if server.stk_pending:
|
||||
_arm_menu_timeout()
|
||||
else:
|
||||
_cancel_menu_timeout()
|
||||
return resp, 200
|
||||
|
||||
|
||||
class PysimHandler(BaseHTTPRequestHandler):
|
||||
def _send_json(self, data, status=200):
|
||||
self.send_response(status)
|
||||
@@ -1552,6 +1665,7 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
global _CARD_CONNECTED
|
||||
self.server.stk_pending = None
|
||||
self.server.menu_active = False
|
||||
_cancel_menu_timeout()
|
||||
self.server.event_list = None
|
||||
_reset_proactive_log()
|
||||
self.server.card = self.server.app.card
|
||||
@@ -1624,6 +1738,7 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
sys.stderr.write('RESCUE: re-sending TERMINAL PROFILE\n')
|
||||
self.server.stk_pending = None
|
||||
self.server.menu_active = False
|
||||
_cancel_menu_timeout()
|
||||
self.server.event_list = None
|
||||
_reset_proactive_log()
|
||||
sm, el = _send_terminal_profile(scc, self.server.terminal_profile)
|
||||
@@ -1902,31 +2017,9 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
env_hex = '%sc20000%02x%s' % (scc.cat_cla, len(menu_tlv), menu_tlv.hex())
|
||||
data, sw = scc._tp.send_apdu(env_hex)
|
||||
resp = {'type': 'done', 'sw': sw}
|
||||
on_fetch = _make_menu_fetch_handler(self.server, resp)
|
||||
if sw.startswith('91'):
|
||||
def _on_menu_fetch(raw, cmd_num, cmd_type, dev_src, dev_dst):
|
||||
if cmd_type == 0x21:
|
||||
text = _parse_display_text(raw) if raw else None
|
||||
if text:
|
||||
self.server.stk_pending = {'type': 'display_text',
|
||||
'cmd_num': cmd_num, 'cmd_type': cmd_type,
|
||||
'dev_src': dev_src, 'dev_dst': dev_dst, 'text': text}
|
||||
resp.update(type='display_text', text=text)
|
||||
return 'pause'
|
||||
elif cmd_type == 0x24:
|
||||
items = _parse_select_item(raw) if raw else []
|
||||
self.server.stk_pending = {'type': 'select_item',
|
||||
'cmd_num': cmd_num, 'cmd_type': cmd_type,
|
||||
'dev_src': dev_src, 'dev_dst': dev_dst, 'items': items}
|
||||
resp.update(type='select_item', items=items)
|
||||
return 'pause'
|
||||
elif cmd_type == 0x25:
|
||||
items = _parse_setup_menu_items(raw) if raw else []
|
||||
self.server.stk_pending = {'type': 'select_item',
|
||||
'cmd_num': cmd_num, 'cmd_type': cmd_type,
|
||||
'dev_src': dev_src, 'dev_dst': dev_dst, 'items': items}
|
||||
resp.update(type='select_item', items=items)
|
||||
return 'pause'
|
||||
_handle_proactive_chain(scc, sw, _on_menu_fetch)
|
||||
_handle_proactive_chain(scc, sw, on_fetch)
|
||||
else:
|
||||
self.server.menu_active = False
|
||||
self.server.stk_pending = None
|
||||
@@ -1935,73 +2028,18 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
st_data, st_sw = _send_status(scc)
|
||||
sys.stderr.write('STATUS -> %s\n' % st_sw)
|
||||
if st_sw.startswith('91'):
|
||||
_handle_proactive_chain(scc, st_sw, _on_menu_fetch)
|
||||
_handle_proactive_chain(scc, st_sw, on_fetch)
|
||||
if self.server.stk_pending:
|
||||
_arm_menu_timeout()
|
||||
else:
|
||||
_cancel_menu_timeout()
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
elif self.path == '/api/menu-respond':
|
||||
scc = self.server.scc
|
||||
if not self.server.stk_pending:
|
||||
self._send_json({'error': 'no pending command'}, 400)
|
||||
return
|
||||
body = self._read_body()
|
||||
self._log_req(body)
|
||||
result = body.get('result', 'ok')
|
||||
item_id = body.get('item_id')
|
||||
RESULT_MAP = {'ok': 0x00, 'cancel': 0x10, 'back': 0x11, 'timeout': 0x12}
|
||||
gr = RESULT_MAP.get(result, 0x00)
|
||||
pd = self.server.stk_pending
|
||||
# Build TERMINAL RESPONSE
|
||||
cd = bytes([0x81, 0x03, pd['cmd_num'], pd['cmd_type'], 0x00])
|
||||
di = bytes([0x82, 0x02, pd['dev_dst'], pd['dev_src']])
|
||||
tr_data = cd + di
|
||||
if isinstance(item_id, int) and result == 'ok' and pd['type'] == 'select_item':
|
||||
tr_data += bytes([0x90, 0x01, item_id])
|
||||
tr_data += bytes([0x83, 0x02, gr, 0x00])
|
||||
tr_hex = '%s140000%02x%s' % (scc.cat_cla, len(tr_data), tr_data.hex())
|
||||
tr_rv = scc._tp.send_apdu(tr_hex)
|
||||
sys.stderr.write('TR(menu): cmd=%02x type=%02x result=%02x -> %s\n' % (pd['cmd_num'], pd['cmd_type'], gr, tr_rv[1]))
|
||||
for entry in reversed(_PROACTIVE_LOG):
|
||||
if (entry.get('cmd_num') == pd['cmd_num']
|
||||
and entry.get('type_hex') == '%02x' % pd['cmd_type']
|
||||
and 'tr_hex' not in entry):
|
||||
_record_tr(entry, tr_data, tr_rv[1])
|
||||
break
|
||||
sw = tr_rv[1]
|
||||
resp = {'sw': sw}
|
||||
if result == 'cancel':
|
||||
self.server.stk_pending = None
|
||||
self.server.menu_active = False
|
||||
else:
|
||||
self.server.stk_pending = None
|
||||
if sw.startswith('91'):
|
||||
def _on_menu_fetch(raw, cmd_num, cmd_type, dev_src, dev_dst):
|
||||
if cmd_type == 0x21:
|
||||
text = _parse_display_text(raw) if raw else None
|
||||
if text:
|
||||
self.server.stk_pending = {'type': 'display_text',
|
||||
'cmd_num': cmd_num, 'cmd_type': cmd_type,
|
||||
'dev_src': dev_src, 'dev_dst': dev_dst, 'text': text}
|
||||
resp.update(type='display_text', text=text)
|
||||
return 'pause'
|
||||
elif cmd_type == 0x24:
|
||||
items = _parse_select_item(raw) if raw else []
|
||||
self.server.stk_pending = {'type': 'select_item',
|
||||
'cmd_num': cmd_num, 'cmd_type': cmd_type,
|
||||
'dev_src': dev_src, 'dev_dst': dev_dst, 'items': items}
|
||||
resp.update(type='select_item', items=items)
|
||||
return 'pause'
|
||||
elif cmd_type == 0x25:
|
||||
items = _parse_setup_menu_items(raw) if raw else []
|
||||
self.server.stk_pending = {'type': 'select_item',
|
||||
'cmd_num': cmd_num, 'cmd_type': cmd_type,
|
||||
'dev_src': dev_src, 'dev_dst': dev_dst, 'items': items}
|
||||
resp.update(type='select_item', items=items)
|
||||
return 'pause'
|
||||
_handle_proactive_chain(scc, sw, _on_menu_fetch)
|
||||
else:
|
||||
self.server.menu_active = False
|
||||
resp['type'] = 'done'
|
||||
self._send_json(resp)
|
||||
resp, code = _menu_send_response(self.server, body.get('result', 'ok'), body.get('item_id'))
|
||||
self._send_json(resp, code)
|
||||
self._log_resp(resp)
|
||||
elif self.path == '/api/event-send':
|
||||
scc = self.server.scc
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for the paused-command (STK menu) timeout watchdog."""
|
||||
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
PROJECTS = Path(__file__).resolve().parents[2]
|
||||
PY_SIM = PROJECTS / 'pysim'
|
||||
if str(PY_SIM) not in sys.path:
|
||||
sys.path.insert(0, str(PY_SIM))
|
||||
|
||||
import pysim_otaman_server.server as S
|
||||
|
||||
|
||||
class TestMenuTimeout(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.saved = (S._MENU_TIMEOUT, S._MENU_TIMER)
|
||||
|
||||
def tearDown(self):
|
||||
S._cancel_menu_timeout()
|
||||
S._MENU_TIMEOUT, S._MENU_TIMER = self.saved
|
||||
|
||||
def test_clamped(self):
|
||||
S._set_menu_timeout(30)
|
||||
self.assertEqual(S._MENU_TIMEOUT, 30)
|
||||
S._set_menu_timeout(0)
|
||||
self.assertEqual(S._MENU_TIMEOUT, 0)
|
||||
S._set_menu_timeout(-1)
|
||||
self.assertEqual(S._MENU_TIMEOUT, 0)
|
||||
S._set_menu_timeout(99999)
|
||||
self.assertEqual(S._MENU_TIMEOUT, 3600)
|
||||
|
||||
def test_arm_starts_timer(self):
|
||||
S._set_menu_timeout(30)
|
||||
with mock.patch.object(S.threading, 'Timer') as timer:
|
||||
S._arm_menu_timeout()
|
||||
timer.assert_called_once_with(30, S._menu_timeout_fire)
|
||||
|
||||
def test_zero_disables_arming(self):
|
||||
S._set_menu_timeout(0)
|
||||
with mock.patch.object(S.threading, 'Timer') as timer:
|
||||
S._arm_menu_timeout()
|
||||
timer.assert_not_called()
|
||||
|
||||
def test_cancel(self):
|
||||
timer = mock.Mock()
|
||||
S._MENU_TIMER = timer
|
||||
S._cancel_menu_timeout()
|
||||
timer.cancel.assert_called_once()
|
||||
self.assertIsNone(S._MENU_TIMER)
|
||||
|
||||
|
||||
class TestMenuSendResponse(unittest.TestCase):
|
||||
def test_timeout_tr_is_flat_with_general_result(self):
|
||||
sent = []
|
||||
|
||||
def send_apdu(hexstr):
|
||||
sent.append(hexstr)
|
||||
return ('', '9000')
|
||||
|
||||
server = types.SimpleNamespace(
|
||||
stk_pending={'type': 'display_text', 'cmd_num': 1, 'cmd_type': 0x21,
|
||||
'dev_src': 0x81, 'dev_dst': 0x83},
|
||||
menu_active=True,
|
||||
scc=types.SimpleNamespace(cat_cla='80', _tp=types.SimpleNamespace(send_apdu=send_apdu)),
|
||||
)
|
||||
resp, code = S._menu_send_response(server, 'timeout', None)
|
||||
self.assertEqual(code, 200)
|
||||
self.assertEqual(resp['sw'], '9000')
|
||||
self.assertEqual(resp['type'], 'done')
|
||||
self.assertIsNone(server.stk_pending)
|
||||
self.assertFalse(server.menu_active)
|
||||
tr = sent[0]
|
||||
self.assertTrue(tr.startswith('801400000d'), tr)
|
||||
self.assertIn('8103012100', tr)
|
||||
self.assertIn('82028381', tr)
|
||||
self.assertIn('83021200', tr)
|
||||
|
||||
def test_no_pending_returns_400(self):
|
||||
resp, code = S._menu_send_response(types.SimpleNamespace(stk_pending=None), 'ok')
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn('error', resp)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user