a4d5415034
New eSIM pill (Phone simulator) for SGP.22/32 cards, built on pySim's ES10 static API — no lpac, no new dependencies, no pysim patches, no SM-DP+ interaction: - Chip: EID, EUICCInfo1/2, configured addresses (ES10a/b). - Profiles: GetProfilesInfo with metadata (state, nickname, provider, ICCID, ISD-P AID, class, owner, icon). - Notifications: read-only ListNotification viewer. - Switch: Enable/DisableProfile with RefreshFlag=1; the card's REFRESH (fetched/answered/logged by the transport's proactive handler) or an ok result triggers _esim_reinit() — reset + equip + _apply_equipped_card — so the ICCID, network state and cached views are re-read. - Server: pysim_simple_server/esim.py, GET /api/esim/chip|profiles| notifications, POST /api/esim/profile (all under _CARD_LOCK; 400 not_an_euicc), euicc/eid in /api/status. - Tests: tests/test_esim.py (fake scc, monkeypatched store_data_tlv), frontend/tests/esim.test.js; help EN/RU, docs/api.md, AGENTS.
197 lines
7.7 KiB
Python
197 lines
7.7 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 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)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|