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:
@@ -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'))
|
||||
|
||||
@@ -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], [])
|
||||
|
||||
Reference in New Issue
Block a user