diff --git a/docs/api.md b/docs/api.md index 18d27b0..39ec2f0 100644 --- a/docs/api.md +++ b/docs/api.md @@ -232,7 +232,9 @@ lock; `GET /api/status` reports `euicc` and `eid` for the PWA. card sent no icon. - `GET /api/esim/notifications` — `{"notifications": [{"seq_number": 3, "operations": ["enable"], "address": "smdp.example.org", - "iccid": "8970…"}], "error": null}`. + "iccid": "8970…"}], "error": null}`. `operations` are the decoded + `ProfileMgmtOperation` flags (`install`, `enable`, `disable`, `delete`, in + that order) — one or more per notification. - `POST /api/esim/profile` — `{"action": "enable"|"disable", "iccid"?: …, "isdp_aid"?: …, "refresh"?: true}` (one identifier required). With the refresh flag set the ISD-R returns OK *before* the REFRESH (SGP.22 v2.6 diff --git a/pysim_simple_server/esim.py b/pysim_simple_server/esim.py index 5999ee9..cf3056a 100644 --- a/pysim_simple_server/esim.py +++ b/pysim_simple_server/esim.py @@ -436,6 +436,21 @@ def profiles(app): return {'profiles': out, 'error': None} +# ProfileMgmtOperation flags in bit order (SGP.22 §5.7.9). The TLV carries a +# padding-bits octet followed by the flags octet, so a TLV parsed from the +# card nests the flags under 'pmo' while an object built from decoded flags +# carries them directly. +PROFILE_MGMT_OPERATIONS = ('install', 'enable', 'disable', 'delete') + + +def _profile_operations(op): + """ProfileMgmtOperation flags -> operation names ([] when unknown).""" + if not isinstance(op, dict): + return [] + flags = op.get('pmo') if isinstance(op.get('pmo'), dict) else op + return [name for name in PROFILE_MGMT_OPERATIONS if flags.get(name)] + + def notifications(app): """ES10b ListNotification: read-only list of pending notifications.""" resp = _transceive(app, ListNotificationReq(), ListNotificationResp) @@ -446,11 +461,9 @@ def notifications(app): lst = flat.get('notification_metadata_list') out = [] for n in _repeated(lst, 'notification_metadata'): - op = n.get('profile_mgmt_operation') - operations = sorted(k for k, v in op.items() if v) if isinstance(op, dict) else [] out.append({ 'seq_number': n.get('seq_number'), - 'operations': operations, + 'operations': _profile_operations(n.get('profile_mgmt_operation')), 'address': n.get('notification_address'), 'iccid': n.get('iccid'), }) diff --git a/tests/test_esim.py b/tests/test_esim.py index 3936669..ba258a6 100644 --- a/tests/test_esim.py +++ b/tests/test_esim.py @@ -161,6 +161,34 @@ class EsimTests(unittest.TestCase): 'address': 'smdp.example.org', 'iccid': '8970119000004002667', }]) + def test_notifications_parses_operations_from_the_card_tlv(self): + # The card's ProfileMgmtOperation TLV (padding octet + flags octet, + # SGP.22 5.7.9) nests the flags under 'pmo' when pySim parses it. + app, _ = make_app() + address = '6D6E6F2D30302E6573696D73657276696365732E636F6D' # mno-00.esimservices.com + meta = _tlv(0xBF2F, _tlv(0x80, '00') + _tlv(0x81, '0140') + + _tlv(0x0C, address) + _tlv(0x5A, '980711090000042066F7')) + resp = ListNotificationResp() + resp.from_tlv(bytes.fromhex(_tlv(0xBF28, _tlv(0xA0, meta)))) + self.patch([resp]) + out = esim.notifications(app) + self.assertIsNone(out['error']) + self.assertEqual(out['notifications'], [{ + 'seq_number': 0, 'operations': ['enable'], + 'address': 'mno-00.esimservices.com', + 'iccid': '8970119000004002667', + }]) + + def test_profile_operations_handles_both_shapes(self): + self.assertEqual(esim._profile_operations( + {'pmo': {'install': False, 'enable': True, 'disable': False, + 'delete': True}}), ['enable', 'delete']) + self.assertEqual(esim._profile_operations( + {'install': True, 'enable': False, 'disable': False, 'delete': False}), + ['install']) + self.assertEqual(esim._profile_operations(None), []) + self.assertEqual(esim._profile_operations('pmo'), []) + def test_chip_info_collects_parts_and_errors(self): app, _ = make_app() self.patch_eid('89049032000000000000000000000001')