Files
simple/tests/test_menu_timeout.py
T
catarrh 547ea61708 v3.25.2: the TR tag-form alignment with TS 102 223 6.8.0/6.8.1
The TR's response objects are all Min = N in the spec's own table, and
6.8.0 says such objects "should set the CR flag to comprehension not
required" - so the builders now emit the base tag forms:

- device identities 82 -> 02 (every builder)
- PLI data 93/94/AD/AE/BF/E2/E5 -> 13/14/2D/2E/3F/62/65 (26/63 already
  base); the PWA inline codecs follow
- POLL INTERVAL Duration 84 -> 04
- GET INKEY/INPUT Text string 8D -> 0D
- SELECT ITEM Item identifier 90 -> 10
- Timer identifier/value A4/A5 -> 24/25
(_bip_tr already used the reference traces' 01/02/03.)

The command details now echo the command's tag form (01/81) besides
its qualifier - 6.8.1 requires the object to be identical to the
command's "including the comprehension required flag": the parser
returns cd_tag and _build_tr, _handle_timer_command,
_handle_bip_command, the menu pendings and testscript.build_tr carry
it.  The Result keeps 03/83 (Min = Y).  _tr_data_only strips both tag
styles and the POLL Duration decode accepts 04.

Tests: the base forms per builder + the echo + the PWA drift guards;
883 frontend + 866 Python; sw cache simple-v426.
2026-10-11 13:31:49 +03:00

129 lines
4.3 KiB
Python

#!/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_simple_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('02028381', 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()
class TestFinishPendingMenu(unittest.TestCase):
def make_server(self):
return types.SimpleNamespace(
stk_pending={'type': 'select_item', 'cmd_num': 1, 'cmd_type': 0x24,
'dev_src': 0x81, 'dev_dst': 0x83, 'items': []},
menu_active=True, scc=None)
def make_scc(self, sent, sw='9000'):
return types.SimpleNamespace(
cat_cla='80',
_tp=types.SimpleNamespace(send_apdu=lambda h: (sent.append(h) or ('', sw))))
def test_no_pending_is_noop(self):
sent = []
S._finish_pending_menu(types.SimpleNamespace(stk_pending=None), self.make_scc(sent))
self.assertEqual(sent, [])
def test_pending_finished_with_cancel_tr(self):
server = self.make_server()
sent = []
scc = self.make_scc(sent)
server.scc = scc
S._finish_pending_menu(server, scc)
self.assertEqual(len(sent), 1)
tr = sent[0]
self.assertTrue(tr.startswith('801400000d'), tr)
self.assertIn('83021000', tr) # general result 0x10 = cancel
self.assertIsNone(server.stk_pending)
self.assertFalse(server.menu_active)
def test_91xx_answer_drains_chain(self):
server = self.make_server()
scc = self.make_scc([], sw='9120')
server.scc = scc
with mock.patch.object(S, '_handle_proactive_chain') as chain:
S._finish_pending_menu(server, scc)
chain.assert_called_once_with(scc, '9120')