diff --git a/docs/api.md b/docs/api.md index 3a4701f..3b8daaa 100644 --- a/docs/api.md +++ b/docs/api.md @@ -666,7 +666,8 @@ server resolves it from the preset store, so a run always uses the stored keysets, TARs and counters. The inline `preset` form stays for external callers. -**Action steps** (`type: "action"`): `kind` is `envelope` (`event`, `data`), +**Action steps** (`type: "action"`): `kind` is `envelope` (`event`, `data`, +optional `src` - the device-identities source override), `event` (`event` name or hex + `fields`, optional `src` - the semantic event download for 0x03/0x0B/0x12/0x1D), `menu-select` (`item_id` 1-255, or `text` + `mode` `exact`/`contains` with an diff --git a/docs/test-scripts.md b/docs/test-scripts.md index 638ae15..39cfce8 100644 --- a/docs/test-scripts.md +++ b/docs/test-scripts.md @@ -14,7 +14,8 @@ localStorage export imports as-is), and any API client can create or run them **Format**: server ≥ 3.15.0 (`kvn`, `por` and `files` checks), scripts stored server-side since 3.16.0, `menu-select` by text and the spec-order TERMINAL -RESPONSE codings since 3.17.0, the semantic `event` action since 3.18.0. +RESPONSE codings since 3.17.0, the semantic `event` action since 3.18.0 and +the `envelope` action's `src` override since 3.18.1. ## Writing a test for an applet @@ -131,7 +132,7 @@ continue correctly. | kind | params | notes | |---|---|---| -| `envelope` | `event` 0–255, `data` hex (optional) | ENVELOPE(Event Download) with raw data | +| `envelope` | `event` 0–255, `data` hex (optional), optional `src` | ENVELOPE(Event Download) with raw data; `src` overrides the device-identities source (`82` terminal / `83` network) | | `event` | `event` (name or hex), `fields` (object), optional `src` | semantic ENVELOPE(Event Download) for 0x03/0x0B/0x12/0x1D (see below) | | `menu-select` | `item_id` 1–255 **or** `text` + `mode` `exact`/`contains` (+ `case_sensitive`), not both | ENVELOPE(Menu Selection); the text is resolved against the card's cached menu - refreshed whenever the card sends SET UP MENU, so it follows the applet's install parameters | | `file-write` | `path`, `data`, `mode` `auto`/`binary`/`record`, `record` 1–255 | UPDATE BINARY/RECORD | @@ -147,7 +148,8 @@ or file names - e.g. `MF/7F20/6F07`, `ADF.USIM/EF.TEST`. The server builds the event objects for the events a test needs most; an event without a builder goes through the raw `envelope` action, whose data hex -the Phone tab's event form shows (**Event data (hex)**, copyable). +the Phone tab's event form shows (**Event data (hex)**, copyable). Both +actions take the same optional `src` device-identities override. ```json {"type": "action", "kind": "event", @@ -177,8 +179,8 @@ the default is the terminal, matching the Phone tab's forms. - `sw` - default `{"mode": "exact", "value": "9000"}` (a `status` action with `attempts > 1` defaults to the `91??` mask). - `data` - the response data (for SCP80: the inline PoR packet). -- `por` - `"any"` (default, no check), `"ok"` (`response_status == por_ok`), - `"none"` (no PoR expected). +- `por` (SCP80 actions only) - `"any"` (default, no check), `"ok"` + (`response_status == por_ok`), `"none"` (no PoR expected). - A plain hex string is exact; a value containing `?` is a mask. **A mask must have exactly the same length as the actual value** (`?` is a per-nibble wildcard) - e.g. `91??` matches `9102`, `027100????` matches a 4-byte diff --git a/frontend/index.html b/frontend/index.html index 79ecdb6..7661645 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -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.0'; +const SIMPLE_VERSION = '3.18.1'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -7328,6 +7328,7 @@ function testStepRender() { if (step.kind === 'envelope') { html += testFormRow('Event type', testFormInput('test-f-event', p.event === undefined ? 3 : p.event, '0..255')); html += testFormRow('Event data (hex, optional)', testFormInput('test-f-data', p.data, '9B0100')); + html += testFormRow('Source override (hex, optional)', testFormInput('test-f-eventsrc', p.src, '82 terminal / 83 network')); } else if (step.kind === 'event') { // The semantic event download: the four events the server builds // (others go through the raw envelope action with the hex the @@ -7537,7 +7538,12 @@ function testStepCollect() { const kind = val('test-step-kind') || step.kind; step.kind = kind; const p = {}; - if (kind === 'envelope') { p.event = num('test-f-event', 3); p.data = hexNoMask('test-f-data'); } + if (kind === 'envelope') { + p.event = num('test-f-event', 3); + p.data = hexNoMask('test-f-data'); + const src = hexNoMask('test-f-eventsrc'); + if (src) p.src = src; + } else if (kind === 'menu-select') { const text = val('test-f-itemtext'); if (text) { @@ -21110,6 +21116,7 @@ const LANG_RU = { 'event is required': 'нужен тип события', 'event fields must be an object': 'поля события должны быть объектом', 'Event data (hex)': 'Данные события (hex)', + 'Source override (hex, optional)': 'Переопределение источника (hex, опционально)', 'Could not load the test scripts from the server': 'Не удалось загрузить тест-скрипты с сервера', 'Could not save the test script': 'Не удалось сохранить тест-скрипт', 'Could not delete the test script': 'Не удалось удалить тест-скрипт', diff --git a/frontend/sw.js b/frontend/sw.js index d12273f..f5dcb38 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v381'; +const CACHE = 'simple-v382'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/testscripts.test.js b/frontend/tests/testscripts.test.js index 23dc095..5931e21 100644 --- a/frontend/tests/testscripts.test.js +++ b/frontend/tests/testscripts.test.js @@ -209,6 +209,16 @@ test('the menu-select editor collects the item id or the text', () => { assert.deepStrictEqual(_testEditStep.params, { item_id: 3 }); }); +test('the envelope step collects the source override', () => { + _testEditStep = { type: 'action', kind: 'envelope', params: {} }; + _testEditChecks = []; + _testEditStepIndex = 0; + fakeForm({ 'test-step-kind': 'envelope', 'test-f-event': '3', 'test-f-data': '9B0100', + 'test-f-eventsrc': '83', 'test-f-sw': '', 'test-f-cdata': '', 'test-f-fail': 'error' }); + testStepCollect(); + assert.deepStrictEqual(_testEditStep.params, { event: 3, data: '9B0100', src: '83' }); +}); + test('the event step collects the type and the JSON fields', () => { _testEditStep = { type: 'action', kind: 'event', params: {} }; _testEditChecks = []; diff --git a/pyproject.toml b/pyproject.toml index c649dc5..57d595b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.18.0" +version = "3.18.1" 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. diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index 6c8cceb..8b55bdb 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -34,7 +34,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.18.0' +VERSION = '3.18.1' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE @@ -5953,7 +5953,7 @@ def _test_run_action(server, step, ctx): if kind == 'envelope': data, sw = _send_event_download(scc, p['event'], bytes.fromhex(p['data']) if p['data'] else None, - drain=False) + drain=False, src=p.get('src')) sent = 'ENVELOPE(Event Download) type=0x%02X' % p['event'] elif kind == 'event': # The semantic event download: the builder owns the byte layout. diff --git a/pysim_simple_server/testscript.py b/pysim_simple_server/testscript.py index fd98f89..f1b326d 100644 --- a/pysim_simple_server/testscript.py +++ b/pysim_simple_server/testscript.py @@ -218,8 +218,15 @@ def _normalise_params(kind, p): if kind == 'envelope': if p.get('event') is None: raise ScriptError('envelope: event is required') - return {'event': _int(p['event'], 'envelope event', 0, 255), - 'data': _data_hex(p.get('data'), 'envelope data', allow_empty=True)} + out = {'event': _int(p['event'], 'envelope event', 0, 255), + 'data': _data_hex(p.get('data'), 'envelope data', allow_empty=True)} + src = str(p.get('src') or '').strip().upper() + if src: + if not re.fullmatch(r'[0-9A-F]{2}', src): + raise ScriptError('envelope: src must be one hex byte ' + '(82 terminal / 83 network)') + out['src'] = src + return out if kind == 'event': # The semantic event download: the builder owns the field set and the # byte layout (events.py mirrors the PWA's forms for the supported diff --git a/tests/test_testscript.py b/tests/test_testscript.py index d08004f..052f13b 100644 --- a/tests/test_testscript.py +++ b/tests/test_testscript.py @@ -167,6 +167,15 @@ class TestValidation(unittest.TestCase): with self.assertRaises(T.ScriptError): T.normalise_script({'steps': [{'type': 'action', 'kind': 'event', 'params': {'event': 3, 'src': 'zz'}}]}, _resolver) + # the raw envelope action carries the same source override + script = T.normalise_script({'steps': [ + {'type': 'action', 'kind': 'envelope', + 'params': {'event': 0x00, 'data': '', 'src': '83'}}]}, _resolver) + self.assertEqual(script['steps'][0]['params'], + {'event': 0x00, 'data': '', 'src': '83'}) + with self.assertRaises(T.ScriptError): + T.normalise_script({'steps': [{'type': 'action', 'kind': 'envelope', + 'params': {'event': 3, 'src': 'zz'}}]}, _resolver) def test_scp80_keyset_number_and_new_content_checks(self): # kvn (1..15) selects the preset's keyset; `por`/`files` are expect @@ -533,6 +542,15 @@ class TestRunnerActions(RunnerTestCase): self.assertEqual(run['status'], 'ok', run['steps']) apdu = scc2.sent[0].upper() self.assertIn('99010B82028381' + 'BF0108', apdu) + # the raw envelope action takes the same source override + scc3 = FakeScc() + scc3.push('80C2', '', '9000') + run = self.run_script(FakeServer(scc3), [ + {'type': 'action', 'kind': 'envelope', + 'params': {'event': 0x00, 'data': '', 'src': '83'}}, + ]) + self.assertEqual(run['status'], 'ok', run['steps']) + self.assertIn('99010082028381', scc3.sent[0].upper()) def test_scp80_uses_the_preset_and_advances_the_counter(self): scc = FakeScc()