Files
simple/tests/test_esim.py
T
catarrh 563b211e15 fix: serve the eSIM GET endpoints from _do_GET; release v3.0.0
The three eSIM GET routes were inserted after the POST-only
/api/verify-adm branch, i.e. into the _do_POST chain, so GET
/api/esim/chip|profiles|notifications fell through to the 404 handler
(only POST /api/esim/profile was reachable).

- Moved /api/esim/chip|profiles|notifications into _do_GET (after
  /api/status); /api/esim/profile stays in _do_POST.
- Regression test: inspect the handler sources and assert each route is
  in the right chain.
- Version 3.0.0 (eSIM support is a major update): server/pyproject/PWA
  header, sw.js simple-v227.
2026-09-21 23:27:16 +03:00

211 lines
8.4 KiB
Python

# coding=utf-8
"""Tests for the local eSIM/LPA operations (ES10a/b/c) in esim.py.
The ES10 transport (``CardApplicationISDR.store_data_tlv``/``get_eid``) is
monkeypatched with canned responses, so the mapping to the JSON API and the
ISD-R selection/restore logic are tested without hardware.
"""
import inspect
import unittest
from types import SimpleNamespace
from pySim.euicc import (
AID_ISD_R, CardApplicationISDR, DisableProfileResp, DisableResult,
EnableProfileResp, EnableResult, EuiccConfiguredAddresses, EuiccInfo1,
Iccid, IsdpAid, ListNotificationResp, NotificationAddress,
NotificationMetadata, NotificationMetadataList, ProfileClass, ProfileInfo,
ProfileInfoListResp, ProfileInfoSeq, ProfileMgmtOperation, ProfileNickname,
ProfileOwner, ProfileOwnerPLMN, ProfileState, SeqNumber,
)
from pySim.utils import h2b
from pysim_simple_server import esim
class FakeLchan:
def __init__(self):
self.scc = SimpleNamespace(name='lchan-scc')
self.selected = None
def select_file(self, app):
self.selected = app
def make_app(profile='Consumer eUICC (SGP.22)', isdr=True):
lchan = FakeLchan()
apps = {}
if isdr:
apps[AID_ISD_R.lower()] = SimpleNamespace(name='ISD-R')
rs = SimpleNamespace(profile=profile,
mf=SimpleNamespace(applications=apps),
lchan=[lchan],
resets=0)
def soft_reset():
rs.resets += 1
rs.soft_reset = soft_reset
return SimpleNamespace(rs=rs), lchan
def profile_info(iccid, aid, state=None, nickname=None, cls=None, owner=None):
children = [Iccid(decoded=iccid), IsdpAid(decoded=bytes.fromhex(aid))]
if state:
children.append(ProfileState(decoded=state))
if nickname:
children.append(ProfileNickname(decoded=nickname))
if cls:
children.append(ProfileClass(decoded=cls))
if owner:
children.append(ProfileOwner(children=[ProfileOwnerPLMN(decoded=owner)]))
return ProfileInfo(children=children)
class EsimTests(unittest.TestCase):
def setUp(self):
self._orig_store = CardApplicationISDR.store_data_tlv
self._orig_eid = CardApplicationISDR.get_eid
self.calls = []
def tearDown(self):
CardApplicationISDR.store_data_tlv = self._orig_store
CardApplicationISDR.get_eid = self._orig_eid
def patch(self, responses):
"""Queue canned responses for store_data_tlv (Exception entries raise)."""
def fake(scc, cmd_do, resp_cls, exp_sw='9000'):
self.calls.append(cmd_do)
r = responses.pop(0)
if isinstance(r, Exception):
raise r
return r
CardApplicationISDR.store_data_tlv = staticmethod(fake)
def patch_eid(self, eid):
CardApplicationISDR.get_eid = staticmethod(lambda scc: eid)
def test_is_euicc(self):
app, _ = make_app()
self.assertTrue(esim.is_euicc(app))
other, _ = make_app(profile='UICC')
self.assertFalse(esim.is_euicc(other))
def test_profiles_maps_metadata(self):
app, lchan = make_app()
resp = ProfileInfoListResp(children=[ProfileInfoSeq(children=[
profile_info('8970119000004002667', 'A0000005591010FFFFFFFF8900000100',
state='enabled', nickname='Work', cls='operational',
owner='250-99'),
profile_info('8970119000004002668', 'A0000005591010FFFFFFFF8900000200',
state='disabled', cls='test'),
])])
self.patch([resp])
out = esim.profiles(app)
self.assertIsNone(out['error'])
self.assertEqual(len(out['profiles']), 2)
first = out['profiles'][0]
self.assertEqual(first['iccid'], '8970119000004002667')
self.assertEqual(first['isdp_aid'], 'A0000005591010FFFFFFFF8900000100')
self.assertEqual(first['state'], 'enabled')
self.assertEqual(first['nickname'], 'Work')
self.assertEqual(first['class'], 'operational')
self.assertEqual(first['owner'], '250-99')
self.assertEqual(out['profiles'][1]['state'], 'disabled')
# ISD-R was selected and the previous selection restored
self.assertEqual(lchan.selected.name, 'ISD-R')
self.assertEqual(app.rs.resets, 1)
# the request asks for every ProfileInfo tag
self.assertIn('9F70', self.calls[0].to_tlv().hex().upper())
def test_profiles_error_result(self):
app, _ = make_app()
resp = ProfileInfoListResp()
resp.from_tlv(h2b('BF2D0381017F'))
self.patch([resp])
out = esim.profiles(app)
self.assertEqual(out['profiles'], [])
self.assertEqual(out['error'], 'undefined error')
def test_notifications_maps_operations(self):
app, _ = make_app()
meta = NotificationMetadata(children=[
SeqNumber(decoded=3),
ProfileMgmtOperation(decoded={'install': False, 'enable': True,
'disable': False, 'delete': False}),
NotificationAddress(decoded='smdp.example.org'),
Iccid(decoded='8970119000004002667'),
])
self.patch([ListNotificationResp(
children=[NotificationMetadataList(children=[meta])])])
out = esim.notifications(app)
self.assertIsNone(out['error'])
self.assertEqual(out['notifications'], [{
'seq_number': 3, 'operations': ['enable'],
'address': 'smdp.example.org', 'iccid': '8970119000004002667',
}])
def test_chip_info_collects_parts_and_errors(self):
app, _ = make_app()
self.patch_eid('89049032000000000000000000000001')
info1 = EuiccInfo1()
info1.from_tlv(h2b('BF20058203010203'))
addresses = EuiccConfiguredAddresses()
addresses.from_tlv(h2b('BF3C13800E736D64702E6578616D706C652E6F72678100'))
self.patch([info1, RuntimeError('no info2'), addresses])
out = esim.chip_info(app)
self.assertEqual(out['eid'], '89049032000000000000000000000001')
self.assertEqual(out['info1'], {'svn': '1.2.3'})
self.assertIsNone(out['info2'])
self.assertIn('info2', out['errors'])
self.assertEqual(out['addresses'].get('default_dp_address'), 'smdp.example.o')
def test_set_profile_state_enable_uses_iccid_and_refresh(self):
app, _ = make_app()
self.patch([EnableProfileResp(children=[EnableResult(decoded='ok')])])
out = esim.set_profile_state(app, 'enable', iccid='8970119000004002667')
self.assertTrue(out['ok'])
self.assertEqual(out['result'], 'ok')
tlv = self.calls[0].to_tlv().hex().upper()
self.assertIn('5A0A980711090000042066F7', tlv) # ProfileIdentifier/ICCID
self.assertTrue(tlv.endswith('810101')) # RefreshFlag = 1
def test_set_profile_state_maps_cat_busy(self):
app, _ = make_app()
self.patch([DisableProfileResp(children=[DisableResult(decoded='catBusy')])])
out = esim.set_profile_state(app, 'disable', iccid='8970119000004002667')
self.assertFalse(out['ok'])
self.assertEqual(out['result'], 'catBusy')
self.assertIn('busy', out['message'])
def test_set_profile_state_validation(self):
app, _ = make_app()
with self.assertRaises(esim.EsimError):
esim.set_profile_state(app, 'delete', iccid='8970119000004002667')
with self.assertRaises(esim.EsimError):
esim.set_profile_state(app, 'enable')
with self.assertRaises(esim.EsimError):
esim.set_profile_state(app, 'enable', iccid='abc')
def test_select_isdr_requires_an_euicc(self):
app, _ = make_app(isdr=False)
with self.assertRaises(esim.EsimError):
esim.chip_info(app)
class EsimRoutingTests(unittest.TestCase):
def test_esim_routes_are_in_the_right_http_handlers(self):
from pysim_simple_server import server
get_src = inspect.getsource(server.PysimHandler._do_GET)
post_src = inspect.getsource(server.PysimHandler._do_POST)
for route in ('/api/esim/chip', '/api/esim/profiles',
'/api/esim/notifications'):
self.assertIn("self.path == '%s'" % route, get_src, route)
self.assertNotIn("self.path == '%s'" % route, post_src, route)
self.assertIn("self.path == '/api/esim/profile'", post_src)
self.assertNotIn("self.path == '/api/esim/profile'", get_src)
if __name__ == '__main__':
unittest.main()