diff --git a/frontend/index.html b/frontend/index.html
index f83a653..1e9f268 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1904,7 +1904,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.23.1';
+const SIMPLE_VERSION = '3.23.2';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== view history (the browser Back/Forward buttons) =====
diff --git a/frontend/sw.js b/frontend/sw.js
index 84e7e97..aa91ce0 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simple-v414';
+const CACHE = 'simple-v415';
const URLS = [
'index.html',
'help.html',
diff --git a/pyproject.toml b/pyproject.toml
index deab407..2492189 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
-version = "3.23.1"
+version = "3.23.2"
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 3415e58..f89d30e 100644
--- a/pysim_simple_server/server.py
+++ b/pysim_simple_server/server.py
@@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
-VERSION = '3.23.1'
+VERSION = '3.23.2'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -7738,12 +7738,21 @@ class PysimHandler(BaseHTTPRequestHandler):
self._send_json(resp, 400)
self._log_resp(resp)
return
+ steps = [step for _entry, script in members for step in script['steps']]
+ if any(s['type'] == 'action' and s['kind'] == 'scp80' for s in steps):
+ err = _test_preset_error({'steps': steps}, preset)
+ if err:
+ resp = {'error': err}
+ self._send_json(resp, 400)
+ self._log_resp(resp)
+ return
reset_info = None
if suite.get('start_reset'):
# A suite may start with a card reset: the physical reset + the
# full equip re-initialization, so its scripts run against a
- # freshly initialized card. This runs before the scp80 checks and
- # the ADM gate: the card session changes, so the ADM latch is
+ # freshly initialized card. This runs after the preset checks - a
+ # run that cannot start must not reset the card - and before the
+ # ADM gate: the card session changes, so the ADM latch is
# re-verified against the post-reset card, and the ICCID must
# still match the run's preset (a reset can switch the active
# profile on an eUICC).
@@ -7777,14 +7786,6 @@ class PysimHandler(BaseHTTPRequestHandler):
self._send_json(resp, 400)
self._log_resp(resp)
return
- steps = [step for _entry, script in members for step in script['steps']]
- if any(s['type'] == 'action' and s['kind'] == 'scp80' for s in steps):
- err = _test_preset_error({'steps': steps}, preset)
- if err:
- resp = {'error': err}
- self._send_json(resp, 400)
- self._log_resp(resp)
- return
needed_adm = bool(suite.get('require_adm')) or any(
bool(script.get('require_adm')) for _entry, script in members)
ok, adm_info, adm_err = _ensure_adm(self.server, preset, needed_adm)
diff --git a/tests/test_fetch_routes.py b/tests/test_fetch_routes.py
index 118ee52..863f5b0 100644
--- a/tests/test_fetch_routes.py
+++ b/tests/test_fetch_routes.py
@@ -1702,6 +1702,21 @@ class TestScriptStoreHttpTests(unittest.TestCase):
time.sleep(0.05)
self.assertFalse(snap['running'], snap)
+ def test_a_bad_scp80_preset_refuses_before_the_reset(self):
+ # v3.23.2: the preset checks come first - a suite that cannot start
+ # must not reset the card
+ self.server.app = object()
+ suite = self._suite(name='reset me', start_reset=True)
+ self._post('/api/test/scripts', self._script(
+ suite_id=suite['id'],
+ steps=[{'type': 'action', 'kind': 'scp80',
+ 'params': {'apdu': '80E2900000'}}]))
+ with mock.patch.object(self.srv, '_card_reset_reinit') as r:
+ status, resp = self._post('/api/test/run', {'suite_id': suite['id']})
+ self.assertEqual(status, 400, resp)
+ self.assertIn('incomplete', resp['error'])
+ r.assert_not_called()
+
def test_suite_start_reset_refuses_a_mismatched_iccid(self):
self.server.app = object()
suite = self._suite(name='reset me', start_reset=True)