Files
simple/frontend/tests/netstate.test.js
T
catarrh b1539d7cd5 net: network-state monitor and permanent-rejection FPLMN (v2.7.0)
server:
- netsim: LOCI/PSLOCI dummy builders take a status; the EPSLOCI dummy is
  wiped to 0B F6 + FF per UICC_NAA.md C3; roaming_denied now emulates the
  permanent 'PLMN not allowed' rejection (C3a): status 010, EPSNSC dropped,
  EF.FPLMN append with TS 31.102 4.2.16 shift semantics and a home-PLMN
  guard (HPLMNwAcT/EHPLMN, IMSI fallback), optional 'Rejection: write FPLMN'
  toggle; write steps carry the logical key; SCENARIO_SERVICE map;
  insert_fplmn/fplmn_entries/parse_imsi helpers
- netstate.py: cached per-session monitor state for the 12 network EFs,
  step-based patch, simulated service state, derived location with country/
  operator (optional MCC/MNC list) and roaming class
- server: monitor read at equip right after a readable ICCID (skipped
  otherwise), cleared on card removal; GET /api/net-state and POST
  /api/net-state-refresh; net-sim patches the cache from the written bytes
  and re-reads EF.IMSI; Location-status events set the service state and
  re-read EF.IMSI (multi-IMSI applets)

frontend:
- Phone tab: 'Network state' panel next to Network simulation with the
  simulated service badge (Undefined until simulated; normal/limited/no
  service + rejection marker), location/roaming line, compact per-file
  summaries with full-decode tooltips and a Refresh button; no card polling
- EF decoders: EF.FPLMN (FFFFFF gaps are not terminators) and EF.EHPLMN
- i18n EN/RU, help updated; SW cache simple-v203

tests: 295 Python / 453 frontend
2026-09-20 12:26:17 +03:00

132 lines
5.1 KiB
JavaScript

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 fn of ['netStateStatusWord', 'netStateFlattenTitle', 'netStatePlmnList',
'netStateSummary', 'netStateRender']) {
code += extractFunc(html, fn) + '\n';
}
const CONSTS = {
NET_STATE_FILES: ['[', '];'],
NET_STATE_STATUS: ['{', '};'],
NET_STATE_AREA: ['{', '};'],
NET_STATE_ROAMING: ['{', '};'],
NET_STATE_SERVICE: ['{', '};'],
NET_STATE_SOURCE: ['{', '};'],
};
for (const [name, [open, close]] of Object.entries(CONSTS)) {
const re = new RegExp('const ' + name + ' = \\' + open + '[\\s\\S]*?\\' + close);
const m = re.exec(html);
if (!m) throw new Error('const ' + name + ' not found');
code += m[0].replace(/^const /, 'var ') + '\n';
}
code += 'globalThis.t = s => s;\n';
code += 'globalThis.esc = s => String(s);\n';
code += 'globalThis.efFlatten = d => Object.entries(d || {}).map(([k, v]) => [k, String(v)]);\n';
eval(code);
function setup() {
const els = {};
for (const id of ['netstate-badge', 'netstate-location', 'netstate-area',
'netstate-read', 'netstate-body']) {
els[id] = { className: '', textContent: '', innerHTML: '' };
}
globalThis.document = { getElementById: id => els[id] || null };
return els;
}
function decoder(map) {
globalThis.efDecodeFile = (name, fid, content) =>
map[name] ? { data: map[name] } : null;
}
test('netStateSummary renders decoded file contents compactly', () => {
setup();
decoder({
'EF.LOCI': { tmsi: '3E905D6B', mcc: '262', mnc: '01', lac: '0x6CD7',
tmsi_time: '0xFF', location_update_status: '0x00' },
'EF.FPLMN': { plmns: [{ mcc: '262', mnc: '01' }, { mcc: '246', mnc: '81' }] },
'EF.HPLMNwAcT': { plmns: [{ mcc: '262', mnc: '01', access_tech: 'LTE' }] },
});
const loci = netStateSummary('loci', { present: true, kind: 'transparent', data: 'AA' });
assert.strictEqual(loci.text, 'LAI 262-01/6CD7 · updated');
assert.ok(loci.title.includes('3E905D6B'));
const fplmn = netStateSummary('fplmn', { present: true, kind: 'transparent', data: 'AA' });
assert.strictEqual(fplmn.text, '262-01, 246-81');
const hplmn = netStateSummary('hplmnwact', { present: true, kind: 'transparent', data: 'AA' });
assert.strictEqual(hplmn.text, '262-01 (LTE)');
const absent = netStateSummary('loci', { present: false });
assert.strictEqual(absent.text, '—');
assert.strictEqual(absent.title, 'not present');
});
test('netStateSummary marks rejection statuses', () => {
setup();
decoder({
'EF.LOCI': { mcc: '246', mnc: '81', lac: '0xFFFE', location_update_status: '0x02' },
});
const loci = netStateSummary('loci', { present: true, kind: 'transparent', data: 'AA' });
assert.strictEqual(loci.text, 'LAI 246-81/FFFE · PLMN not allowed');
});
test('netStateRender paints the service badge, location and rows', () => {
const els = setup();
decoder({ 'EF.IMSI': { imsi: '262011234567890' } });
netStateRender({
service: { state: 'limited', source: 'net-sim:roaming_denied' },
read_at: 1700000000,
files: { imsi: { present: true, kind: 'transparent', data: 'AA', source: 'write' } },
network: { location: { mcc: '262', mnc: '01', country: 'Germany',
operator: 'Telekom', roaming: 'guest', rejected: true,
area: 'LAI', lac: '6CD7', source: 'write' } },
});
assert.strictEqual(els['netstate-badge'].textContent, '⚠ Limited service');
assert.ok(els['netstate-badge'].className.includes('text-amber-600'));
assert.ok(els['netstate-location'].innerHTML.includes('262-01'));
assert.ok(els['netstate-location'].innerHTML.includes('Germany'));
assert.ok(els['netstate-location'].innerHTML.includes('Guest (roaming)'));
assert.ok(els['netstate-location'].innerHTML.includes('PLMN not allowed'));
assert.strictEqual(els['netstate-area'].textContent, 'LAI 6CD7');
assert.ok(els['netstate-body'].innerHTML.includes('EF.IMSI'));
assert.ok(els['netstate-body'].innerHTML.includes('262011234567890'));
assert.ok(els['netstate-body'].innerHTML.includes('>write<'));
});
test('netStateRender shows Undefined until something is simulated', () => {
const els = setup();
decoder({});
netStateRender(null);
assert.strictEqual(els['netstate-badge'].textContent, '○ Undefined');
assert.ok(els['netstate-badge'].className.includes('text-gray-400'));
assert.ok(els['netstate-body'].innerHTML.includes('EF.LOCI'));
assert.ok(els['netstate-body'].innerHTML.includes('—'));
});
test('netStateRender shows the green normal-service badge', () => {
const els = setup();
decoder({});
netStateRender({ service: { state: 'normal' }, files: {} });
assert.strictEqual(els['netstate-badge'].textContent, '● Normal service');
assert.ok(els['netstate-badge'].className.includes('text-emerald-600'));
});