feat: test script engine and server-side runner (phase 1)
A test script drives a deterministic dialogue with the card: action steps (ENVELOPE event / Menu Selection, raw APDU, SCP80 secured packet with a card-preset, file update/read, STATUS) with SW/data/PoR checks, and proactive-command expectations that fetch, check (command type, qualifier, text/item/raw) and answer with a scripted TERMINAL RESPONSE. - pysim_simple_server/testscript.py: pure engine (validation, exact/mask matchers with '?' nibble wildcards, item/text checks, TERMINAL RESPONSE building). - server.py: worker thread + state, /api/test/run|status|stop|clear, the card-endpoint guard (409 while running), background STATUS polling suspended, 'error terminates / warning continues', a pending command is drained with a cancel TR on stop/error, no-drain modes for the ENVELOPE and SCP80 senders (the pending command belongs to the next expectation). - Expectations never poll: a command must be pending (91XX) from the previous step, otherwise it is an error (TS 102 221 7.4.2.1 / TS 102 223 6.3); scripts add an explicit `status` action (attempts/interval) when the card delivers on poll. - SCP80 steps require a complete card preset, may override TAR/SPI1/SPI2 only, and the counter is advanced per send and reported (`scp80_counter`) for the PWA to write back. - tests/test_testscript.py (26 tests: engine, matchers, TR building, the STK menu dialogue, error/warning termination, status polling, unexpected-command drain, SCP80 preset/counter, file actions, guards). - docs/api.md endpoint reference. 467 Python / 573 frontend green.
This commit is contained in:
+80
@@ -39,6 +39,10 @@ a 3.x PWA).
|
||||
| `/api/send-ota` | POST | SCP80 OTA secured packet delivery |
|
||||
| `/api/ram-install` | POST | Install a Java Card `.cap` file via SCP80 (INSTALL[for load] → LOAD ×N → INSTALL[for install]) |
|
||||
| `/api/cap-info` | POST | Validate a `.cap` archive and estimate its code/NVRAM/RAM requirements (read-only) |
|
||||
| `/api/test/run` | POST | Start a test script (actions + proactive expectations) |
|
||||
| `/api/test/status` | GET | Test script run state and per-step results |
|
||||
| `/api/test/stop` | POST | Request a running test script to stop |
|
||||
| `/api/test/clear` | POST | Clear the finished run report |
|
||||
| `/api/sp-verify` | POST | Verify secured packet against pySim reference |
|
||||
| `/api/menu` | GET | Current STK menu (title + items + active) |
|
||||
| `/api/menu-select` | POST | ENVELOPE(Menu Selection) with item_id |
|
||||
@@ -362,6 +366,82 @@ the structural parse but not the analyzer. The numbers are an estimate:
|
||||
the model assumes 2-byte references, a 6-byte object header and NVM cell
|
||||
rounding, and does not include applet-created runtime objects/arrays.
|
||||
|
||||
### `POST /api/test/run`
|
||||
|
||||
Runs a **test script**: an ordered list of actions and proactive-command
|
||||
expectations, executed server-side on the equipped card. While a run is
|
||||
active the card is owned by the script - other card endpoints answer
|
||||
`409 {"error": "test script running ..."}` and background STATUS polling is
|
||||
suspended; only `/api/test/*`, `/api/status`, `/api/poll-status`,
|
||||
`/api/version` and static files stay available.
|
||||
|
||||
```json
|
||||
{"script": {"name": "STK menu browsing", "steps": [
|
||||
{"type": "action", "kind": "menu-select", "params": {"item_id": 128},
|
||||
"check": {"sw": "91??"}},
|
||||
{"type": "expect", "command": "SELECT ITEM",
|
||||
"checks": [{"kind": "item", "id": 1, "text": "test"}],
|
||||
"respond": {"result": "ok", "item_id": 1}},
|
||||
{"type": "expect", "command": "DISPLAY TEXT",
|
||||
"checks": [{"kind": "text", "value": "hello"}],
|
||||
"respond": {"result": "ok"}}
|
||||
]},
|
||||
"preset": {"name": "lab card", "kic": "15", "kid": "15", "kicKey": "...",
|
||||
"kidKey": "...", "counter": "0000000A", "tar": "B00000",
|
||||
"spi1": "16", "spi2": "01"}}
|
||||
```
|
||||
|
||||
**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`, 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`
|
||||
(`attempts`, `interval_ms` - when `attempts > 1` the default SW check is the
|
||||
mask `91??`, i.e. poll until the card announces a command).
|
||||
|
||||
`check` is `{"sw": ..., "data": ...}` (exact or `{"mode": "mask", "value":
|
||||
"91??"}`, `?` = per-nibble wildcard) plus `"por": "none"|"ok"|"any"` for
|
||||
SCP80. `on_fail` is `error` (terminates the script) or `warning` (continues).
|
||||
|
||||
**Expectation steps** (`type: "expect"`) require a command pending from the
|
||||
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
|
||||
value, `item_id`, `text`+`dcs`, raw TLVs).
|
||||
|
||||
The response is the initial state (`running: true`), the final counter
|
||||
(`scp80_counter`) and the step list; poll `/api/test/status`. The PWA writes
|
||||
`scp80_counter` back to the card preset after the run.
|
||||
|
||||
### `GET /api/test/status`
|
||||
|
||||
The current (or last) run:
|
||||
|
||||
```json
|
||||
{"running": true, "name": "STK menu browsing", "status": null,
|
||||
"index": 1, "total": 3, "scp80_counter": null,
|
||||
"steps": [{"index": 0, "type": "action", "label": "ENVELOPE(Menu Selection)",
|
||||
"status": "ok", "sw": "9103", "sent": "80C2000009...",
|
||||
"checks": [{"label": "SW", "ok": true, "expected": "91??",
|
||||
"actual": "9103", "level": "error"}], "ms": 4}]}
|
||||
```
|
||||
|
||||
`status` becomes `ok`/`warning`/`error`/`stopped` when the run finishes;
|
||||
expected/actual pairs are reported per check.
|
||||
|
||||
### `POST /api/test/stop`
|
||||
|
||||
Requests a stop (`{"stop": true}` is set on the run); the runner finishes the
|
||||
current step, answers any pending proactive command with a cancel TERMINAL
|
||||
RESPONSE and reports the run as `stopped`.
|
||||
|
||||
### `POST /api/test/clear`
|
||||
|
||||
Clears a finished run report (409 while a run is active).
|
||||
|
||||
### `POST /api/ram-install`
|
||||
|
||||
Install a Java Card `.cap` file on the card via GlobalPlatform commands (INSTALL[for load] → LOAD ×N → INSTALL[for install (+ make selectable)]) wrapped in SCP80 secured packets. Each step is sent via ENVELOPE and its PoR is checked; the sequence aborts on the first PoR error. The `.cap` archive (a ZIP of nested components) is parsed server-side in `_cap_parse`; no external tooling is required.
|
||||
|
||||
Reference in New Issue
Block a user