v3.22.2: the runner's TERMINAL RESPONSE inherits the terminal's own objects
Two reported gaps: - a scripted PROVIDE LOCAL INFORMATION expectation answered with an empty TR - the PLI dictionary (and the host clock for the date/time qualifier) was silently inert during runs while the poll path used it all along, so an applet waiting for its data stalled behind a passing expectation. A POLL INTERVAL expectation likewise never echoed the Duration (6.8.4). testscript.build_tr() gained `answer_tlvs` (inserted after the Result per the 6.8.0 order, before text/item/raw) and the shared _test_terminal_response() wires the expect step, the proactive-drain action and the run cleanup to it; the script's `raw` overrides, and a non-success result gets no data object. - a GP INSTALL/LOAD/DELETE step always failed the default exact-9000 check although the envelope SW 91XX is the successful answer (the re-announced SET UP MENU; XX is the pending command's length, never pinnable). Documented rather than loosening the default: a new "Driving a RAM install from a script" subsection in docs/test-scripts.md (the 9??? mask + por any, the per-step pending rule - a proactive-drain/expect after every 91XX step), a sentence at the sw-default bullet, and an scp80 default-pinning test. Docs: api.md's respond clause, help EN/RU's TR Config note. Tests: 839 frontend + 812 Python; sw cache simple-v406.
This commit is contained in:
@@ -56,6 +56,17 @@ class TestValidation(unittest.TestCase):
|
||||
]}, _resolver)
|
||||
self.assertEqual(script['steps'][0]['check']['sw'], {'mode': 'exact', 'value': '9000'})
|
||||
|
||||
def test_scp80_defaults_to_an_exact_9000_sw(self):
|
||||
# The default must not drift to a mask: a GP INSTALL/LOAD/DELETE
|
||||
# envelope answers 91XX and the script gives those steps the 9???
|
||||
# mask explicitly (the docs recipe); applet scripts keep asserting
|
||||
# exact 9000.
|
||||
script = T.normalise_script({'steps': [
|
||||
{'type': 'action', 'kind': 'scp80', 'params': {'apdu': '80E2900000'}},
|
||||
]}, _resolver)
|
||||
self.assertEqual(script['steps'][0]['check']['sw'],
|
||||
{'mode': 'exact', 'value': '9000'})
|
||||
|
||||
def test_rejects_bad_scripts(self):
|
||||
bad = [
|
||||
'not a dict',
|
||||
@@ -405,6 +416,26 @@ class TestBuildTr(unittest.TestCase):
|
||||
tr = T.build_tr(2, 0x21, 0x82, 0x81, {'result': 0x00})
|
||||
self.assertEqual(tr.hex().upper(), '81030221008202828183020000')
|
||||
|
||||
def test_the_terminal_answer_objects_follow_the_result(self):
|
||||
# The runner passes the terminal's own objects (the PLI data, the
|
||||
# POLL INTERVAL echo) - they sit right after the Result, before the
|
||||
# text/item/raw objects (TS 102 223 6.8.0).
|
||||
tr = T.build_tr(1, 0x26, 0x82, 0x81, {'result': 0x00},
|
||||
answer_tlvs='2607AABBCCDDEEFF')
|
||||
self.assertEqual(tr.hex().upper(),
|
||||
'810301260082028281830200002607AABBCCDDEEFF')
|
||||
# with a text object the answer object keeps its place
|
||||
tr = T.build_tr(1, 0x23, 0x82, 0x81,
|
||||
{'result': 0x00, 'text': 'hi', 'dcs': 0x04},
|
||||
answer_tlvs='8402011E')
|
||||
self.assertEqual(tr.hex().upper(),
|
||||
'810301230082028281830200008402011E8D03046869')
|
||||
# and the script's raw TLVs stay last
|
||||
tr = T.build_tr(1, 0x26, 0x82, 0x81,
|
||||
{'result': 0x00, 'raw': '9B0100'}, answer_tlvs='2607AA')
|
||||
self.assertEqual(tr.hex().upper(),
|
||||
'810301260082028281830200002607AA9B0100')
|
||||
|
||||
|
||||
# ─── runner with a fake card ─────────────────────────────────────────────
|
||||
|
||||
@@ -755,6 +786,98 @@ class TestRunnerProactiveDrain(RunnerTestCase):
|
||||
self.assertFalse(run['steps'][0]['checks'][0]['ok'])
|
||||
|
||||
|
||||
class TestRunnerTerminalAnswers(RunnerTestCase):
|
||||
"""The runner's TERMINAL RESPONSE inherits the terminal's own objects
|
||||
(v3.22.2 review): the PLI dictionary / host clock and the POLL INTERVAL
|
||||
echo - unless the script's `raw` overrides them."""
|
||||
|
||||
PLI_01 = 'D009810301260182028281' # PROVIDE LOCAL INFORMATION, qual 01
|
||||
PLI_03 = 'D009810301260382028281' # qual 03: date/time
|
||||
POLL = 'D009810301030082028281' # POLL INTERVAL
|
||||
|
||||
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 test_a_pli_expectation_carries_the_dictionary_entry(self):
|
||||
scc = FakeScc()
|
||||
self._pending(scc, self.PLI_01)
|
||||
with mock.patch.dict(S._PLI_DATA, {1: '140853937640650700F4'}):
|
||||
run = self.run_script(FakeServer(scc), [
|
||||
{'type': 'action', 'kind': 'status', 'params': {'attempts': 2}},
|
||||
{'type': 'expect', 'command': 'PROVIDE LOCAL INFORMATION',
|
||||
'respond': {'result': 'ok'}},
|
||||
])
|
||||
self.assertEqual(run['status'], 'ok', run['steps'])
|
||||
self.assertIn('140853937640650700F4', self._tr(scc))
|
||||
|
||||
def test_a_pli_raw_overrides_the_dictionary(self):
|
||||
scc = FakeScc()
|
||||
self._pending(scc, self.PLI_01)
|
||||
with mock.patch.dict(S._PLI_DATA, {1: '140853937640650700F4'}):
|
||||
run = self.run_script(FakeServer(scc), [
|
||||
{'type': 'action', 'kind': 'status', 'params': {'attempts': 2}},
|
||||
{'type': 'expect', 'command': 'PROVIDE LOCAL INFORMATION',
|
||||
'respond': {'result': 'ok', 'raw': '2607AABBCCDDEEFF'}},
|
||||
])
|
||||
self.assertEqual(run['status'], 'ok', run['steps'])
|
||||
tr = self._tr(scc)
|
||||
self.assertIn('2607AABBCCDDEEFF', tr)
|
||||
self.assertNotIn('140853937640650700F4', tr)
|
||||
|
||||
def test_a_pli_date_time_answer_uses_the_host_clock(self):
|
||||
scc = FakeScc()
|
||||
self._pending(scc, self.PLI_03)
|
||||
run = self.run_script(FakeServer(scc), [
|
||||
{'type': 'action', 'kind': 'status', 'params': {'attempts': 2}},
|
||||
{'type': 'expect', 'command': 'PROVIDE LOCAL INFORMATION',
|
||||
'respond': {'result': 'ok'}},
|
||||
])
|
||||
self.assertEqual(run['status'], 'ok', run['steps'])
|
||||
self.assertRegex(self._tr(scc), r'2607[0-9A-F]{14}')
|
||||
|
||||
def test_a_refused_pli_gets_no_data_object(self):
|
||||
scc = FakeScc()
|
||||
self._pending(scc, self.PLI_01)
|
||||
with mock.patch.dict(S._PLI_DATA, {1: '140853937640650700F4'}):
|
||||
run = self.run_script(FakeServer(scc), [
|
||||
{'type': 'action', 'kind': 'status', 'params': {'attempts': 2}},
|
||||
{'type': 'expect', 'command': 'PROVIDE LOCAL INFORMATION',
|
||||
'respond': {'result': 'refused'}},
|
||||
])
|
||||
self.assertEqual(run['status'], 'ok', run['steps'])
|
||||
self.assertNotIn('140853937640650700F4', self._tr(scc))
|
||||
|
||||
def test_a_poll_interval_expectation_echoes_the_duration(self):
|
||||
scc = FakeScc()
|
||||
self._pending(scc, self.POLL)
|
||||
run = self.run_script(FakeServer(scc), [
|
||||
{'type': 'action', 'kind': 'status', 'params': {'attempts': 2}},
|
||||
{'type': 'expect', 'command': 'POLL INTERVAL',
|
||||
'respond': {'result': 'ok'}},
|
||||
])
|
||||
self.assertEqual(run['status'], 'ok', run['steps'])
|
||||
self.assertRegex(self._tr(scc), r'840201[0-9A-F]{2}')
|
||||
|
||||
def test_a_drained_pli_carries_the_dictionary_entry(self):
|
||||
scc = FakeScc()
|
||||
scc.push('80F2', '', '9109') # STATUS: PLI pending
|
||||
scc.push('8012', self.PLI_01, '9000')
|
||||
scc.push('8014', '', '9000')
|
||||
scc.push('80F2', '', '9000') # confirming STATUS: idle
|
||||
with mock.patch.dict(S._PLI_DATA, {1: '140853937640650700F4'}):
|
||||
run = self.run_script(FakeServer(scc), [
|
||||
{'type': 'action', 'kind': 'proactive-drain',
|
||||
'params': {'attempts': 3, 'interval_ms': 0}},
|
||||
])
|
||||
self.assertEqual(run['status'], 'ok', run['steps'])
|
||||
self.assertIn('140853937640650700F4', self._tr(scc))
|
||||
|
||||
|
||||
class TestRunnerActions(RunnerTestCase):
|
||||
def test_script_menu_select_mirrors_menu_active(self):
|
||||
# 9000 -> the menu dialogue is over
|
||||
|
||||
Reference in New Issue
Block a user