v3.15.0: test-script fixes for applet testing - preset id, kvn, PoR and file-list checks
Three gaps blocked SCP80 applet testing from the PWA: - testRunPreset() still marshalled the pre-v3.8 flat preset fields (kic/kid/keys/counter/tar/spi1/spi2, all empty on a stored preset), so every SCP80 run failed with "SCP80 preset is incomplete: no keyset is defined". The PWA sends the matched preset's id now and the server resolves it from the store (_test_run_preset_from_body; the inline `preset` body stays for API callers), so a run always uses the stored keysets, TARs and counters. Regression tests on both sides. - testscript._normalise_params dropped the scp80 step's `kvn` (the editor exposed it and the runner and validation consumed it, so scripts silently used the preset's first keyset). kvn is validated (1-15) and passed through; the client-side problem check bounds it too. - The PoR delivered as a SEND SHORT MESSAGE (PoR-in-submit, SPI2 bit 0x20) is decoded for the expect step that fetches it: the new `por` content check asserts the response status / R-APDU SW / response data, decoded with the preceding scp80 step's context (concatenated SMS accumulate across expectations). The inline transport stays on the action (check.por / check.data). - The new `files` content check asserts the File List of a REFRESH (TS 102 223 8.18, tag 12/92) - e.g. qualifier 00 = NAA initialization with full file change notification - as an order-insensitive set of FID paths. The step editor exposes both checks; help EN/RU, docs/api.md and the new authoring reference docs/test-scripts.md document the format. Tests: 803 frontend + 680 Python; sw cache simple-v378.
This commit is contained in:
+15
-4
@@ -653,13 +653,19 @@ suspended; only `/api/test/*`, `/api/status`, `/api/poll-status`,
|
||||
{"role": "usimRfm", "tar": "B00001", "msl": "16"}]}}
|
||||
```
|
||||
|
||||
The PWA sends `preset_id` (the stored preset's id) instead of `preset`; the
|
||||
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`),
|
||||
`menu-select` (`item_id` 1-255), `file-write` (`path`, `data`, `mode`
|
||||
`auto`/`binary`/`record`, `record`), `file-read` (same, verifies `check.data`),
|
||||
`apdu` (raw transport, no auto-handler), `scp80` (`apdu` or `sp`, selected by
|
||||
the optional `source` field when both are present; optional `tar`/`spi1`/`spi2`
|
||||
overrides - KIc/KID and the counter always come from the `preset`, which must
|
||||
match the equipped card and be complete) or `status`
|
||||
the optional `source` field when both are present; optional `kvn` (keyset
|
||||
number 1-15), `tar`/`spi1`/`spi2` overrides - KIc/KID and the counter always
|
||||
come from the resolved preset, which must match the equipped card and be
|
||||
complete) or `status`
|
||||
(`attempts`, `interval_ms` - when `attempts > 1` the default SW check is the
|
||||
mask `91??`, i.e. poll until the card announces a command).
|
||||
|
||||
@@ -672,7 +678,12 @@ previous step (`91XX`); they never poll - a `9000` response means no command
|
||||
and is an error (TS 102 221 7.4.2.1 / TS 102 223 6.3; add a `status` action
|
||||
if the card delivers on poll). `command` is a name or type code; `checks`
|
||||
may be `text` (contains/exact), `item` (`id`/`text` for SELECT ITEM / SET UP
|
||||
MENU) or `raw` (mask); `respond` is the TERMINAL RESPONSE (`result` name or
|
||||
MENU), `raw` (mask), `por` (a SEND SHORT MESSAGE carrying the PoR of the
|
||||
preceding `scp80` step - PoR-in-submit, SPI2 bit `20`: `status` (the response
|
||||
status name, `ok` = `por_ok`), `sw` (the R-APDU status word, exact/mask) and
|
||||
`data` (the R-APDU response data, exact/mask)) or `files` (the File List of a
|
||||
REFRESH, TS 102 223 8.18; `files` is a list of path hex strings, matched
|
||||
order-insensitively); `respond` is the TERMINAL RESPONSE (`result` name or
|
||||
value, `item_id`, `text`+`dcs`, raw TLVs).
|
||||
|
||||
The response is the initial state (`running: true`), the final counter
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
# Test script authoring guide
|
||||
|
||||
A **test script** is a JSON document that drives a deterministic dialogue with
|
||||
the card over the local server: ordered **actions** (each sends something and
|
||||
checks the response) and **expectations** (each fetches the proactive command
|
||||
the card announced, checks it, and sends the scripted TERMINAL RESPONSE).
|
||||
|
||||
Scripts are imported into the PWA (Simulator → Test script → Import) and run
|
||||
with **Run** against the equipped card, or POSTed directly to
|
||||
`/api/test/run` (see `docs/api.md`). While a run is active the card belongs to
|
||||
the script.
|
||||
|
||||
## Document shape
|
||||
|
||||
```json
|
||||
{"name": "applet RFM update",
|
||||
"steps": [
|
||||
{"type": "action", "kind": "scp80", "params": {…}, "check": {…}, "label": "optional"},
|
||||
{"type": "expect", "command": "REFRESH", "qualifier": "00",
|
||||
"checks": [{"kind": "files", "value": "3F007F206F07"}],
|
||||
"respond": {"result": "ok"}}
|
||||
]}
|
||||
```
|
||||
|
||||
Every step may carry `on_fail`: `"error"` (default - stops the run) or
|
||||
`"warning"` (continues, the report shows ⚠). Each entry of `checks` may
|
||||
override it with its own `on_fail`.
|
||||
|
||||
## Presets: where the keys, TARs and counters come from
|
||||
|
||||
An SCP80 action never carries key material. The PWA sends the **stored
|
||||
preset's id** with the run; the server resolves it and takes:
|
||||
|
||||
- the keyset - the step's `kvn` (1–15) or the preset's first keyset,
|
||||
- the keys and the counter of that keyset,
|
||||
- the TAR table with each TAR's **MSL** (Minimum SPI1).
|
||||
|
||||
So before a run the matched preset (ICCID) must contain the keyset and, for
|
||||
every TAR the script addresses, either a TAR entry with its MSL or an explicit
|
||||
`spi1` in the step. Counter handling: the run starts from the keyset's stored
|
||||
counter and persists each accepted packet's next value, so consecutive runs
|
||||
continue correctly.
|
||||
|
||||
## Actions
|
||||
|
||||
```json
|
||||
{"type": "action", "kind": "…", "params": {…}, "check": {…}}
|
||||
```
|
||||
|
||||
| kind | params | notes |
|
||||
|---|---|---|
|
||||
| `envelope` | `event` 0–255, `data` hex (optional) | ENVELOPE(Event Download) |
|
||||
| `menu-select` | `item_id` 1–255 | ENVELOPE(Menu Selection) |
|
||||
| `file-write` | `path`, `data`, `mode` `auto`/`binary`/`record`, `record` 1–255 | UPDATE BINARY/RECORD |
|
||||
| `file-read` | `path`, `mode`, `record` | READ BINARY/RECORD; verify with `check.data` |
|
||||
| `apdu` | `apdu` hex | raw transport, no auto-handler |
|
||||
| `scp80` | `apdu` **xor** `sp` (optional `source`), `kvn`, `tar`, `spi1`, `spi2` | secured packet to the TAR |
|
||||
| `status` | `attempts` 1–1000, `interval_ms` 0–10000 | with `attempts > 1` the default SW check is `91??` (poll until a command) |
|
||||
|
||||
`path` is `/`-separated: `MF` (or `3F00`) or an ADF name/AID first, then FIDs
|
||||
or file names - e.g. `MF/7F20/6F07`, `ADF.USIM/EF.TEST`.
|
||||
|
||||
### `check` on actions
|
||||
|
||||
```json
|
||||
"check": {"sw": {"mode": "mask", "value": "91??"},
|
||||
"data": {"mode": "exact", "value": "0271…"},
|
||||
"por": "ok"}
|
||||
```
|
||||
|
||||
- `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).
|
||||
- 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
|
||||
prefix.
|
||||
|
||||
## Expectations
|
||||
|
||||
```json
|
||||
{"type": "expect", "command": "SEND SHORT MESSAGE",
|
||||
"qualifier": "00",
|
||||
"checks": [{"kind": "por", "status": "por_ok", "sw": "9000"}],
|
||||
"respond": {"result": "ok"}}
|
||||
```
|
||||
|
||||
The card must have announced a command (`91XX`) in the previous step - the
|
||||
runner never polls by itself. `command` is a name (as shown in the proactive
|
||||
log) or a hex type; `ANY`/`*` skips the type check. `qualifier` is the
|
||||
command-details byte 3 (one byte, exact/mask).
|
||||
|
||||
### Content checks
|
||||
|
||||
| kind | fields | checks |
|
||||
|---|---|---|
|
||||
| `text` | `mode` contains/exact, `value`, `case_sensitive` | alpha identifier / text |
|
||||
| `item` | `id`, `text`, `mode`, `case_sensitive` | items of SELECT ITEM / SET UP MENU |
|
||||
| `raw` | `mode` exact/mask, `value` | the whole fetched command (hex, exact length needed) |
|
||||
| `por` | `status`, `sw`, `data` | the PoR carried by a **SEND SHORT MESSAGE** (see below) |
|
||||
| `files` | `files` (list of path hex strings) or a comma/space separated string | the **File List** of a REFRESH, TS 102 223 §8.18; matched order-insensitively |
|
||||
|
||||
`por` needs an SCP80 step **before** the expectation: the TPDU of the SEND
|
||||
SHORT MESSAGE is decoded with that step's keyset, SPI and counter. `status`
|
||||
is the decoded response status name (`por_ok`, `cntr_low`,
|
||||
`rc_cc_ds_failed`, …; `ok` is accepted for `por_ok`); `sw` is the R-APDU
|
||||
status word and `data` the R-APDU response data (exact/mask) - both come from
|
||||
the decoded response (scripting `AB`/`AF`/compact forms). A PoR split over
|
||||
several SMS parts is accumulated across SEND SHORT MESSAGE expectations: put
|
||||
the `por` check on the step that completes the sequence (intermediate steps
|
||||
should use `raw` or no PoR check).
|
||||
|
||||
### `respond`
|
||||
|
||||
The TERMINAL RESPONSE for the fetched command: `result` (name or hex -
|
||||
`ok`, `partial`, `missing`, `refused`, `not_understood`, `modified`, `cancel`,
|
||||
`back`, `timeout`, `no_response`), `item_id`, `text`+`dcs`, `raw` (extra
|
||||
COMPREHENSION-TLVs before the result).
|
||||
|
||||
## The two PoR transports (SPI2)
|
||||
|
||||
1. **Inline** - the card answers the ENVELOPE `9000` with the PoR in the
|
||||
response data. Verify on the action: `"por": "ok"` and/or
|
||||
`"data": {"mode": "mask", "value": "…"}`.
|
||||
2. **PoR-in-submit** (SPI2 bit `20`, e.g. `spi2: "21"`) - the card announces
|
||||
a **SEND SHORT MESSAGE** (`91XX`); the PoR is inside its SMS TPDU:
|
||||
|
||||
```json
|
||||
{"type": "action", "kind": "scp80",
|
||||
"params": {"apdu": "…", "tar": "B00001", "spi2": "21"},
|
||||
"check": {"sw": {"mode": "mask", "value": "91??"}, "por": "none"}},
|
||||
{"type": "expect", "command": "SEND SHORT MESSAGE",
|
||||
"checks": [{"kind": "por", "status": "por_ok", "sw": "9000"}],
|
||||
"respond": {"result": "ok"}}
|
||||
```
|
||||
|
||||
If the card announces the SEND SHORT MESSAGE only on a later STATUS poll,
|
||||
insert a `status` action (`attempts` > 1) before the expectation.
|
||||
|
||||
## "No actions" after a trigger
|
||||
|
||||
An exact SW check is the assertion: `"check": {"sw": "9000"}` fails if the
|
||||
card answers `91XX` (a command was announced); the runner also errors on a
|
||||
pending command the next step does not expect.
|
||||
|
||||
## Worked examples
|
||||
|
||||
### RFM update → file change → REFRESH (NAA init + full FCN)
|
||||
|
||||
```json
|
||||
{"name": "applet RFM update",
|
||||
"steps": [
|
||||
{"type": "action", "kind": "scp80",
|
||||
"params": {"apdu": "80E2900000", "tar": "B00001", "spi2": "01"},
|
||||
"check": {"sw": {"mode": "mask", "value": "91??"}, "por": "none"},
|
||||
"label": "RFM update"},
|
||||
{"type": "expect", "command": "REFRESH", "qualifier": "00",
|
||||
"checks": [{"kind": "files", "value": "3F007F206F07"}],
|
||||
"respond": {"result": "ok"}},
|
||||
{"type": "action", "kind": "file-read", "params": {"path": "MF/7F20/6F07"},
|
||||
"check": {"data": {"mode": "mask", "value": "AA??CC"}}}
|
||||
]}
|
||||
```
|
||||
|
||||
`qualifier` `00` is "NAA Initialization and Full File Change Notification";
|
||||
the `files` check asserts the exact set of changed files the applet reports.
|
||||
For this mode the terminal must **not** reset the card
|
||||
(TS 102 223 §6.4.7), so the run continues - the following `file-read`
|
||||
verifies the updated content.
|
||||
|
||||
### Incoming data → particular PoR, no other actions
|
||||
|
||||
Inline PoR:
|
||||
|
||||
```json
|
||||
{"name": "applet data (inline PoR)",
|
||||
"steps": [
|
||||
{"type": "action", "kind": "scp80",
|
||||
"params": {"apdu": "80E2900000", "tar": "B00001", "spi2": "01"},
|
||||
"check": {"sw": "9000", "por": "ok",
|
||||
"data": {"mode": "mask", "value": "027100????0102"}}}
|
||||
]}
|
||||
```
|
||||
|
||||
PoR-in-submit (with the transport asserted by the expectation):
|
||||
|
||||
```json
|
||||
{"name": "applet data (SEND SM PoR)",
|
||||
"steps": [
|
||||
{"type": "action", "kind": "scp80",
|
||||
"params": {"apdu": "80E2900000", "tar": "B00001", "spi2": "21"},
|
||||
"check": {"sw": {"mode": "mask", "value": "91??"}, "por": "none"}},
|
||||
{"type": "expect", "command": "SEND SHORT MESSAGE",
|
||||
"checks": [{"kind": "por", "status": "por_ok"}],
|
||||
"respond": {"result": "ok"}}
|
||||
]}
|
||||
```
|
||||
|
||||
## Run errors worth knowing
|
||||
|
||||
- `SCP80 preset is incomplete: …` - the matched preset has no complete
|
||||
keyset; `step N: keyset K is not defined in the preset` - the step's `kvn`
|
||||
is outside the preset's keysets.
|
||||
- `no MSL for TAR … - set it in the Cards tab or pass spi1` - the step's TAR
|
||||
has no entry in the preset and the step carries no `spi1`.
|
||||
- `no proactive command pending …` - an expectation ran without a `91XX` from
|
||||
the previous step (add a `status` action when the card delivers on poll).
|
||||
- `card verdict: no PoR (the card sent none for SPI2 …)` - an SCP80 operation
|
||||
expected a PoR that never arrived (check SPI2 and the transport).
|
||||
|
||||
## Known limitations
|
||||
|
||||
- The runner executes one command at a time and never polls by itself; a
|
||||
proactive command that arrives only on a STATUS poll needs an explicit
|
||||
`status` action.
|
||||
- A `por` check only decodes SEND SHORT MESSAGE commands; the inline transport
|
||||
is checked on the action (`por`/`data`).
|
||||
- `files` matches the File List as a set (order-insensitive); duplicate paths
|
||||
are kept.
|
||||
Reference in New Issue
Block a user