From f87f3e9841e4c39809d1b24c46d5225eadee9a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Tue, 22 Sep 2026 00:30:32 +0300 Subject: [PATCH] esim: decode the notification operations from the card's TLV shape ListNotification showed 'pmo' as the operation for every notification: pySim's ProfileMgmtOperation is a Struct whose first (ignored) byte is the padding-bits octet, so a TLV parsed from the card nests the flags under 'pmo' while an object built from decoded flags (the unit-test path) does not. The mapping iterated the outer dict and reported the nested dict as a truthy key. - esim._profile_operations() accepts both shapes and returns the set flags in spec/bit order (install, enable, disable, delete). - tests: a notification parsed from the card-shaped raw TLV (padding octet included) plus the helper's both-shapes/multi-flag cases. - docs: /api/esim/notifications and the repo AGENTS note. --- docs/api.md | 4 +++- pysim_simple_server/esim.py | 19 ++++++++++++++++--- tests/test_esim.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) 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')