v3.11.0: Cards tab redesign (list + editor), preset PIN/PUK codes

The Cards tab now shows the preset list by default; "Add preset" / "Edit"
open a separate editor view (Save stores and returns to the list, Cancel
discards; ramGoToPreset opens it too).  The editor keeps the name/ICCID
header (with From card) and groups its fields into bordered blocks:
- PIN/ADM: the new optional card codes PIN1/PUK1/PIN2/PUK2 (plain text,
  4-8 decimal digits) plus ADM, moved out of the top row; nothing consumes
  the PIN/PUK codes yet.
- TAR: one uniform table (TAR, MSL, description); the first three rows are
  the fixed role entries with the immutable English descriptions "ISD
  compact format" / "UICC shared FS RFM compact format" / "ADF FS RFM
  compact format"; further rows are deletable with an optional description.
- SCP80 (keyset editor) and SCP81 (PSK pair) as before.

Store: PRESET_FIELDS gains pin1/puk1/pin2/puk2 (optional; whitespace
stripped; 4-8 decimal digits validated on save and import), SCHEMA_VERSION
4, docs/help/AGENTS updated.  TAR rows use unified ids (cards-tr-N-*), the
editor switches via cardsShowList/cardsShowEditor.

Tests: 665 Python + 783 frontend green; the whole inline PWA script
syntax-checked.  sw cache simple-v361.
This commit is contained in:
2026-10-02 09:32:45 +03:00
parent 5dd056beb8
commit 65c565e058
12 changed files with 451 additions and 188 deletions
+26
View File
@@ -211,6 +211,29 @@ class StoreCrudTests(unittest.TestCase):
with self.assertRaises(presets.PresetError):
self.store.add(preset(name=''))
def test_pin_and_puk_codes_are_optional_and_normalised(self):
p = self.store.add(preset())
# absent/empty: all four default to ''
self.assertEqual([p[k] for k in ('pin1', 'puk1', 'pin2', 'puk2')],
['', '', '', ''])
p = self.store.add(preset(name='Codes', pin1=' 12 34 ', puk1='12345678',
pin2='0000', puk2=' 8765 4321 '))
self.assertEqual(p['pin1'], '1234')
self.assertEqual(p['puk1'], '12345678')
self.assertEqual(p['pin2'], '0000')
self.assertEqual(p['puk2'], '87654321')
def test_pin_and_puk_codes_must_be_4_to_8_digits(self):
for field in ('pin1', 'puk1', 'pin2', 'puk2'):
for bad in ('123', '123456789', '12A4', 'abcd'):
with self.assertRaises(presets.PresetError) as ctx:
self.store.add(preset(name='X', **{field: bad}))
self.assertIn(field.upper(), str(ctx.exception))
# 4 and 8 digits are the accepted bounds; empty stays optional
p = self.store.add(preset(name='Bounds', pin1='1234', puk1='12345678'))
self.assertEqual(p['pin1'], '1234')
self.assertEqual(p['puk1'], '12345678')
def test_duplicate_iccid_is_refused_across_stored_forms(self):
self.store.add(preset(iccid=DIGITS))
with self.assertRaises(presets.PresetError) as ctx:
@@ -362,11 +385,14 @@ class StorePersistenceTests(unittest.TestCase):
def test_the_file_round_trips(self):
store = make_store(self.tmp.name)
p = store.add(preset(iccid=DIGITS, pskIdentity='id', pskKey='00' * 16,
pin1='1234', puk1='12345678',
keysets=[keyset(kic='15', kid='15'),
keyset(kic='29', kid='29', cntr='0000000004')]))
raw = json.loads(pathlib.Path(store.path).read_text(encoding='utf-8'))
self.assertEqual(raw['version'], presets.SCHEMA_VERSION)
self.assertEqual(raw['presets'][0]['keysets'][1]['cntr'], '0000000004')
self.assertEqual(raw['presets'][0]['pin1'], '1234')
self.assertEqual(raw['presets'][0]['puk1'], '12345678')
again = make_store(self.tmp.name)
self.assertEqual([x['id'] for x in again.list()], [p['id']])
self.assertEqual(again.find_keyset(p['id'], 2)['cntr'], '0000000004')