net-sim: network-condition simulation in the Phone tab (v2.5.0)
Server (pysim_simple_server/netsim.py + POST /api/net-sim): - pure builders for the observed phone write vocabulary (UICC_NAA.md §13): EPSNSC (A0 TLV with KSI/KASME/counts/algo, padded to the card's FCP record size; invalidate-wipe and invalidate-keep-key), LOCI/PSLOCI/EPSLOCI real and dummy forms (PLMN kept, LAC FFFE, status 01/02), Kc/KcGPRS (9-byte USIM and 33-byte GSM forms, 07 invalidates), EF.SMSstatus counter bump (read-modify-write), CBMI/CBMIR, the Location status event (9B 01 <st>, with the optional 13 07 location info for normal service) and the AUTHENTICATE APDU (TS 31.102 7.1.2.1) with DB/DC response parsing - scenario runner executes the recipes under _CARD_LOCK, picks the first existing candidate path (ADF.USIM vs DF.GSM/DF.TELECOM), reads FCP/current records where the format is card-specific, skips the event unless the card subscribed to Location status, honours the scenario toggles and returns a per-step log; only D6/DC, ENVELOPE and AUTHENTICATE are ever sent - GET /api/mcc-mnc serves the optional worldwide operator list (--mcc-mnc-list, default <workspace>/samples/mcc-mnc-list.json): ?q= search and ?random=1&exclude= for roaming; the list stays out of the repo PWA: - Phone pill gains a 'Network simulation' fieldset: one button per scenario (cold boot, EPS/2G attach, service lost, limited service, roaming denied, churn, SMS received, CB reconfig, AUTHENTICATE), a collapsed Parameters block (operator search + random roaming, LAC/Cell ID/TAC/RAC, optional identities, toggles, churn count/delay) and a step log with SWs - i18n EN/RU, help/README/api.md/AGENTS.md updated; version 2.5.0, SW cache simple-v193 Tests: tests/test_netsim.py (19), frontend/tests/netsim.test.js (3) and html guards - 423 frontend / 280 Python, all passing
This commit is contained in:
@@ -202,6 +202,21 @@ test('every help anchor used by the UI exists in help.html', () => {
|
||||
assert.ok(html.includes("? 'scp81-scripts' : 'scp81-listener'"));
|
||||
});
|
||||
|
||||
test('phone simulator has the network-simulation fieldset', () => {
|
||||
assert.ok(html.includes('data-l10n="Network simulation"'));
|
||||
for (const s of ['cold_boot', 'attach_eps', 'attach_2g', 'service_lost',
|
||||
'limited_service', 'roaming_denied', 'churn', 'sms_received',
|
||||
'cb_reconfig', 'authenticate']) {
|
||||
assert.ok(html.includes("netSimRun('" + s + "')"), s);
|
||||
}
|
||||
assert.ok(html.includes('id="netsim-op-search"'));
|
||||
assert.ok(html.includes('id="netsim-log"'));
|
||||
assert.ok(html.includes('id="netsim-status"'));
|
||||
assert.match(html, /id="netsim-mcc" value="001"/);
|
||||
// scenario buttons need the card (the server writes real EFs)
|
||||
assert.match(html, /data-needs="card" onclick="netSimRun\('service_lost'\)"/);
|
||||
});
|
||||
|
||||
test('SCP81 listener exposes its four modes with the matching notes', () => {
|
||||
for (const v of ['tls', 'redirect', 'passthru', 'dump']) {
|
||||
assert.ok(html.includes('value="' + v + '"'), v);
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
||||
|
||||
function extractFunc(src, name) {
|
||||
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
||||
const m = re.exec(src);
|
||||
if (!m) throw new Error('function ' + name + ' not found');
|
||||
let i = m.index + m[0].length - 1;
|
||||
let depth = 0;
|
||||
for (; i < src.length; i++) {
|
||||
if (src[i] === '{') depth++;
|
||||
else if (src[i] === '}') {
|
||||
depth--;
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
return src.slice(m.index, i + 1);
|
||||
}
|
||||
|
||||
let code = '';
|
||||
for (const f of ['netSimRandomHex', 'netSimDice', 'netSimVal', 'netSimChk', 'netSimParams', 'netSimStepText']) {
|
||||
code += extractFunc(html, f) + '\n';
|
||||
}
|
||||
code += 'function t(k){return k;}\n';
|
||||
code += 'var _fields = {};\nvar document = { getElementById: function(id){ return _fields[id] || null; } };\n';
|
||||
eval(code);
|
||||
|
||||
test('netSimRandomHex produces uppercase hex of the requested length', () => {
|
||||
const h = netSimRandomHex(4);
|
||||
assert.match(h, /^[0-9A-F]{8}$/);
|
||||
assert.match(netSimRandomHex(1), /^[0-9A-F]{2}$/);
|
||||
});
|
||||
|
||||
test('netSimParams maps form fields and checkboxes', () => {
|
||||
_fields = {
|
||||
'netsim-mcc': { value: '262' },
|
||||
'netsim-mnc': { value: '01' },
|
||||
'netsim-lac': { value: ' 6CD7 ' },
|
||||
'netsim-lac-empty': { value: '' },
|
||||
'netsim-churn-count': { value: '5' },
|
||||
'netsim-send-event': { checked: true },
|
||||
'netsim-dummy-locations': { checked: false },
|
||||
'netsim-invalidate-epsnsc': { checked: true },
|
||||
'netsim-keep-kasme': { checked: true },
|
||||
'netsim-write-kc': { checked: false },
|
||||
'netsim-sms-location': { checked: true },
|
||||
'netsim-cb-clear': { checked: false },
|
||||
};
|
||||
const p = netSimParams();
|
||||
assert.strictEqual(p.mcc, '262');
|
||||
assert.strictEqual(p.mnc, '01');
|
||||
assert.strictEqual(p.lac, '6CD7');
|
||||
assert.strictEqual(p.churn_count, '5');
|
||||
assert.strictEqual(p.send_event, true);
|
||||
assert.strictEqual(p.dummy_locations, false);
|
||||
assert.strictEqual(p.invalidate_epsnsc, true);
|
||||
assert.strictEqual(p.keep_kasme, true);
|
||||
assert.strictEqual(p.write_kc, false);
|
||||
assert.strictEqual(p.sms_location, true);
|
||||
assert.strictEqual(p.cb_clear, false);
|
||||
// unset fields are omitted so the server fills its own defaults
|
||||
assert.ok(!('tmsi' in p));
|
||||
assert.ok(!('kasme' in p));
|
||||
});
|
||||
|
||||
test('netSimStepText renders the step log lines', () => {
|
||||
assert.strictEqual(
|
||||
netSimStepText({ action: 'update_binary', file: 'loci', path: 'ADF.USIM/6F7E', data: 'FF01', sw: '9000' }),
|
||||
'update_binary loci (ADF.USIM/6F7E) FF01 → SW 9000');
|
||||
assert.strictEqual(
|
||||
netSimStepText({ action: 'update_record', file: 'epsnsc', path: 'ADF.USIM/6FE4', data: 'A0', sw: '9000' }),
|
||||
'update_record epsnsc (ADF.USIM/6FE4) A0 → SW 9000');
|
||||
assert.strictEqual(
|
||||
netSimStepText({ action: 'event', file: 'location_status', data: '9B0102', sw: '9000' }),
|
||||
'ENVELOPE location_status 9B0102 → SW 9000');
|
||||
assert.strictEqual(
|
||||
netSimStepText({ action: 'skip', file: 'location_status', note: 'event 0x03 not in SET UP EVENT LIST' }),
|
||||
'skip location_status — event 0x03 not in SET UP EVENT LIST');
|
||||
assert.match(
|
||||
netSimStepText({ action: 'authenticate', parsed: { type: 'synchronisation_failure' }, sw: '9000', response: 'DC10AA' }),
|
||||
/^AUTHENTICATE synchronisation_failure → SW 9000 DC10AA$/);
|
||||
});
|
||||
Reference in New Issue
Block a user