v3.27.0 review: the outfile import hardening

- The endpoint validates algorithm16 (3des2/aes, case-insensitive) and mode
  (merge/replace or omitted): a misspelled value answers 400 instead of
  silently picking 3DES2 or turning an import into a dry run.  The choices
  come from outfile.ALGORITHM16_CHOICES, derived from the algorithm map, so
  the validation and the parser cannot drift apart.
- A present but unusable PIN/PUK value is reported with its raw text: a value
  that strips to nothing (e.g. 'abcd') used to vanish without a report line,
  contradicting the "nothing is silently dropped" contract.  A column absent
  from a short row stays silent - it is not present.
- The column-line detection drops the arbitrary 8-character column-name cap:
  a candidate header must carry ICCID plus at least one column the import
  consumes (a fixed name or a numbered KIC/KID), so a long unknown column
  name next to a real one parses while 'ICCID <value>' and
  'ICCID CUSTOMER_NAME' config lines are still rejected.  Both real samples
  are unaffected.
- PWA: a chosen default keyset survives an empty or failed preview (it used
  to reset to null, and the import silently fell back to the card's first
  keyset); the select renders the preview's keysets plus a remembered value.
- PWA: the destructive Replace mode asks for confirmation first, matching the
  other store-wiping actions.
- api.md documents both validations.  Tests: the endpoint's 400s and the
  replace import, the raw-value report, the header guards, the keyset
  preservation and the declined confirmation.  906 frontend + 904 Python;
  sw cache simple-v437.
This commit is contained in:
2026-10-14 13:50:30 +03:00
parent 212bfc24ac
commit 21d7f58f61
8 changed files with 163 additions and 23 deletions
+15
View File
@@ -955,6 +955,21 @@ class PresetStoreHttpTests(unittest.TestCase):
status, resp = self._post('/api/presets/import-outfile', {
'text': text, 'tar_keyset': 99})
self.assertEqual(status, 400, resp)
# a misspelled algorithm or mode is refused, never silently defaulted
status, resp = self._post('/api/presets/import-outfile', {
'text': text, 'algorithm16': 'aes-256'})
self.assertEqual(status, 400, resp)
self.assertIn('algorithm16', resp['error'])
status, resp = self._post('/api/presets/import-outfile', {
'text': text, 'mode': 'Merge'})
self.assertEqual(status, 400, resp)
self.assertIn('mode', resp['error'])
# replace wipes the store first
status, resp = self._post('/api/presets/import-outfile', {
'text': text, 'name': 'Batch 1', 'mode': 'replace'})
self.assertEqual(status, 200, resp)
self.assertEqual(resp['added'], 1)
self.assertEqual(len(self.store.list()), 1)
def test_the_counter_an_operation_accepted_is_persisted(self):
p = self.store.add(self._preset(iccid='8970119000004600098'))