Files
simple/docs/test-scripts.md
T
catarrh 8430dd1694 v3.16.0: test scripts move to a server-side store
The scripts lived in the browser's localStorage although only the local
server can run them.  The new test_scripts.py store
(~/.pysim-simple-server/test_scripts.json, --test-scripts PATH) sits next to
the card presets: stable uuids and timestamps, atomic writes, and every
mutation is validated with the runner's own engine (the submitted shape is
kept - the normaliser's output is not re-normalisable: it fills defaults
like an 'any' PoR check that the check normaliser then rejects), so a stored
script cannot fail at run start for a validation reason.  Import accepts the
old localStorage export as-is (export/import remains the migration path; the
PWA import also takes an exported array).

- /api/test/scripts (GET card-free: path/count + scripts), POST create,
  /update, /delete and /import serve the store; the endpoints answer 503
  without a store and 500 on a write failure, like the preset endpoints.
- /api/test/run takes a stored script_id (resolved when no inline script is
  sent); the PWA sends the edited copy inline plus the id for the report,
  and the run snapshot carries script_id.
- The PWA's Test script pill loads from the store, writes through on edits
  (debounced name, immediate step/new/copy/delete), reports load/save/delete
  failures inline, and exports the portable {name, steps} shape.

Tests: 808 frontend + 701 Python; sw cache simple-v379.
2026-10-04 17:24:21 +03:00

224 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 live in the **server-side store**
(`~/.pysim-simple-server/test_scripts.json`, `--test-scripts`): the PWA's Test
script pill manages them (Import/Export move JSON in and out, the old
localStorage export imports as-is), and any API client can create or run them
(`POST /api/test/run` with an inline `script` or a stored `script_id` - 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.