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
+27
View File
@@ -82,6 +82,21 @@ class ColumnDetectionTests(unittest.TestCase):
self.assertEqual(res['report']['cards'], 1)
self.assertEqual(res['presets'][0]['iccid'], DIGITS)
def test_a_header_without_a_usable_column_is_not_the_column_line(self):
# a line carrying ICCID and nothing the import can consume is not the
# header (every card needs a KICn/KIDn pair)
text = ('ICCID CUSTOMER_NAME\nICCID KIC1 KID1\n' + DIGITS_F + ' ' +
K16A + ' ' + K16B + '\n')
cols, i = outfile.find_columns(text)
self.assertEqual(i, 1)
self.assertEqual(cols, ['ICCID', 'KIC1', 'KID1'])
# ...while a long unknown column name next to a real one is fine
text = ('ICCID TRANSPORT_KEY_IDENTIFIER KIC1 KID1\n' + DIGITS_F + ' ' +
'AB ' + K16A + ' ' + K16B + '\n')
cols, i = outfile.find_columns(text)
self.assertEqual(i, 0)
self.assertEqual(len(cols), 4)
class AlfaShapeTests(unittest.TestCase):
def test_the_rows_become_presets_with_three_keysets(self):
@@ -229,3 +244,15 @@ class OptionTests(unittest.TestCase):
self.assertEqual(len(res['presets']), 1)
self.assertNotIn('pin1', res['presets'][0])
self.assertTrue(any('PIN1' in s for s in res['report']['skipped']))
# a present but unusable value is named with its raw text (a value that
# strips to nothing must not vanish silently)
text = ('ICCID KIC1 KID1 PUK1\n' + DIGITS_F + ' ' + K16A + ' ' + K16B + ' abcd\n')
res = outfile.parse_outfile(text)
self.assertNotIn('puk1', res['presets'][0])
self.assertTrue(any("PUK1 'abcd'" in s for s in res['report']['skipped']))
# ...while a column absent from a short row stays silent (not present)
text = ('ICCID KIC1 KID1 PIN1 PUK1\n' + DIGITS_F + ' ' + K16A + ' ' + K16B + ' 1234\n')
res = outfile.parse_outfile(text)
self.assertEqual(res['presets'][0]['pin1'], '1234')
self.assertNotIn('puk1', res['presets'][0])
self.assertEqual([s for s in res['report']['skipped'] if 'PUK1' in s], [])