// network.test.js — node test for the pure network module (iwd D-Bus JSON // shaping). Run: node network.test.js (must print "network.test.js: ALL PASS") var assert = require('assert'); var fs = require('fs'); var path = require('path'); var N = require('./network.js'); var FIX = path.join(__dirname, 'test-fixtures', 'iwd'); function fixture(name) { return fs.readFileSync(path.join(FIX, name), 'utf8'); } var passed = 0; function eq(got, want, msg) { assert.strictEqual(got, want, msg); passed++; } function ok(cond, msg) { assert(cond, msg); passed++; } var GMO = fixture('get-managed-objects.json'); var GON = fixture('ordered-networks.json'); // --- unwrap: peels busctl's { type, data: [value] } wrapper ----------------- eq(N.unwrap('{"type":"u","data":[42]}'), 42, 'unwrap pulls the single payload'); eq(N.unwrap('{"type":"a","data":[]}'), null, 'unwrap of empty data => null'); eq(N.unwrap(null), null, 'unwrap(null) => null'); // --- signalBars: dBm*100 -> 0..4 (thresholds match iwctl rendering) --------- eq(N.signalBars(-4900), 4, '-49 dBm => 4 bars'); eq(N.signalBars(-6000), 4, '-60 dBm boundary => 4 bars'); eq(N.signalBars(-6500), 3, '-65 dBm => 3 bars'); eq(N.signalBars(-6800), 2, '-68 dBm => 2 bars'); eq(N.signalBars(-8400), 1, '-84 dBm => 1 bar'); eq(N.signalBars(-10000), 1, 'very weak => 1 bar (never 0 for a listed net)'); eq(N.signalBars('not-a-number'), 0, 'non-finite => 0 bars'); // --- parseManaged: GetManagedObjects -> structured ------------------------- var managed = N.parseManaged(GMO); ok(/\/0\/4$/.test(managed.devicePath), 'devicePath is the station device object'); eq(managed.device.name, 'wlan0', 'device name discovered (not hardcoded)'); eq(managed.device.powered, true, 'device powered'); eq(managed.device.mode, 'station', 'device mode station'); eq(managed.station.state, 'connected', 'station state connected'); eq(managed.station.scanning, false, 'station not scanning'); ok(/686f6d652d77696669_psk$/.test(managed.station.connectedNetwork), 'connectedNetwork is home-wifi path'); eq(Object.keys(managed.networks).length, 11, 'eleven networks in managed objects'); ok('home-wifi' in managed.knownBySsid, 'home-wifi is a known network'); ok('home-wifi-old' in managed.knownBySsid, 'home-wifi-old known (not currently in scan)'); eq(Object.keys(managed.knownBySsid).length, 2, 'exactly two known networks'); // --- parseOrdered: GetOrderedNetworks -> [{path,signal}] ------------------- var ordered = N.parseOrdered(GON); eq(ordered.length, 11, 'eleven ordered networks'); eq(ordered[0].signal, -6800, 'first ordered net signal = -6800 (connected first)'); ok(/686f6d652d77696669_psk$/.test(ordered[0].path), 'first ordered path is home-wifi'); // --- networkRows: join managed + ordered -> view rows ---------------------- var rows = N.networkRows(managed, ordered); eq(rows.length, 11, 'eleven view rows'); function row(ssid) { return rows.filter(function (r) { return r.ssid === ssid; })[0] || null; } var home = row('home-wifi'); ok(home !== null, 'home-wifi present'); eq(home.signal, 2, 'home-wifi 2 bars'); eq(home.connected, true, 'home-wifi connected'); eq(home.known, true, 'home-wifi known'); eq(home.needsPassphrase, false, 'connected/known net needs no passphrase'); ok(typeof home.path === 'string' && home.path.length > 0, 'row carries D-Bus object path'); var fiveg = row('home-wifi-5g'); eq(fiveg.signal, 4, 'home-wifi-5g 4 bars'); eq(fiveg.known, false, 'home-wifi-5g not known'); eq(fiveg.needsPassphrase, true, 'unknown psk net needs passphrase'); var cafe = row('Cafe Guest WiFi'); ok(cafe !== null, 'spaced SSID "Cafe Guest WiFi" preserved verbatim'); eq(cafe.signal, 3, 'Cafe Guest WiFi 3 bars'); var city = row('City Hotspot'); ok(city !== null, 'open spaced SSID "City Hotspot" present'); eq(city.security, 'open', 'City Hotspot is open'); eq(city.needsPassphrase, false, 'open net needs no passphrase'); eq(rows[0].ssid, 'home-wifi', 'iwd order preserved: connected network first'); // --- robustness: malformed / empty inputs must NOT throw ------------------- var emptyManaged = N.parseManaged('{"type":"a{oa{sa{sv}}}","data":[{}]}'); eq(emptyManaged.devicePath, '', 'no device => empty devicePath'); eq(emptyManaged.device, null, 'no device => null'); eq(emptyManaged.station, null, 'no station => null'); eq(Object.keys(emptyManaged.networks).length, 0, 'no networks => empty'); eq(N.parseManaged(null).devicePath, '', 'parseManaged(null) safe'); eq(N.parseOrdered(null).length, 0, 'parseOrdered(null) => []'); eq(N.parseOrdered('{"type":"a(on)","data":[[]]}').length, 0, 'empty ordered => []'); eq(N.networkRows({ networks: {} }, []).length, 0, 'no rows from empty inputs'); // an ordered path with no matching managed network is skipped, not crashed eq(N.networkRows({ networks: {} }, [{ path: '/ghost', signal: -5000 }]).length, 0, 'orphan ordered path skipped'); // --- racing snapshots: reconcile by canonical identity -------------------- var churnManaged = { station: { connectedNetwork: '/managed/home' }, networks: { '/managed/home': { ssid: 'home', security: 'psk', connected: true, known: true }, '/managed/cafe': { ssid: 'cafe', security: 'open', connected: false, known: false } } }; var churnRows = N.networkRows(churnManaged, [ { path: '/ordered/cafe', signal: -5100, ssid: 'cafe', security: 'open' }, { path: '/ordered/home', signal: -7000, ssid: 'home', security: 'psk' } ]); eq(churnRows.length, 2, 'path churn keeps usable ordered rows'); eq(churnRows[0].ssid, 'cafe', 'ordered identity preserves iwd ordering'); eq(churnRows.filter(function (r) { return r.connected; }).length, 1, 'connected identity emitted once'); var fallbackRows = N.networkRows(churnManaged, [ { path: '/managed/cafe', signal: -5100 } ]); eq(fallbackRows.length, 2, 'managed connected network missing from ordered reply is retained'); eq(fallbackRows[0].ssid, 'home', 'connected fallback is placed first'); eq(fallbackRows[0].signal, 0, 'connected fallback has deterministic unknown signal'); // --- view helpers ---------------------------------------------------------- eq(N.signalRole(0), 'ui.minor', 'role weak'); eq(N.signalRole(1), 'ui.minor', 'role 1 bar weak'); eq(N.signalRole(2), 'ui.accent', 'role medium'); eq(N.signalRole(3), 'ui.accent', 'role 3 medium'); eq(N.signalRole(4), 'content.accent', 'role strong'); eq(N.securityLabel('psk'), 'wpa', 'psk label'); eq(N.securityLabel('open'), 'open', 'open label'); eq(N.securityLabel('8021x'), 'enterprise', 'enterprise label'); eq(N.needsPassphrase('open', false), false, 'open never needs passphrase'); eq(N.needsPassphrase('psk', true), false, 'known psk needs no passphrase'); eq(N.needsPassphrase('psk', false), true, 'unknown psk needs passphrase'); eq(N.networkId('home-wifi'), 'home-wifi', 'networkId is the ssid'); ok(N.networkId('').length > 0, 'networkId never empty'); ok(N.signalGlyph(0).length > 0, 'glyph 0 non-empty'); ok(N.signalGlyph(4).length > 0, 'glyph 4 non-empty'); // --- dbmFromCenti + signalDbm on rows -------------------------------------- eq(N.dbmFromCenti(-6500), -65, 'centiDbm -> dBm'); eq(N.dbmFromCenti(-6849), -68, 'centiDbm rounds'); eq(N.dbmFromCenti('x'), null, 'non-finite => null'); eq(home.signalDbm, -68, 'home-wifi row carries dBm (-6800 -> -68)'); ok(typeof fiveg.signalDbm === 'number' && fiveg.signalDbm < 0, 'fiveg signalDbm is a negative dBm number'); // --- bandLabel / rateLabel ------------------------------------------------- eq(N.bandLabel(5220), '5 GHz', '5220 MHz => 5 GHz'); eq(N.bandLabel(2412), '2.4 GHz', '2412 MHz => 2.4 GHz'); eq(N.bandLabel(6195), '6 GHz', '6195 MHz => 6 GHz'); eq(N.bandLabel(0), '', 'zero freq => empty'); eq(N.rateLabel(1350), '135 Mbps', '1350 (100kbit/s) => 135 Mbps'); eq(N.rateLabel(1080), '108 Mbps', '1080 => 108 Mbps'); eq(N.rateLabel(0), '', 'zero rate => empty'); // --- connectionDetail: StationDiagnostic a{sv} (synthetic, sanitized BSSID) - var DIAG = JSON.stringify({ type: 'a{sv}', data: [{ ConnectedBss: { type: 's', data: '00:aa:bb:cc:dd:01' }, Frequency: { type: 'u', data: 5220 }, Channel: { type: 'q', data: 44 }, Security: { type: 's', data: 'WPA2-Personal' }, RSSI: { type: 'n', data: -65 }, AverageRSSI: { type: 'n', data: -66 }, RxMode: { type: 's', data: '802.11n' }, TxBitrate: { type: 'u', data: 1080 }, RxBitrate: { type: 'u', data: 1350 } }] }); var det = N.connectionDetail(DIAG); ok(det !== null, 'connectionDetail returns an object for a connected BSS'); eq(det.bssid, '00:aa:bb:cc:dd:01', 'bssid carried verbatim'); eq(det.band, '5 GHz', 'band derived from frequency'); eq(det.channel, 44, 'channel carried'); eq(det.rssi, -65, 'rssi is plain dBm (not centi)'); eq(det.rxRate, '135 Mbps', 'rx rate converted'); eq(det.txRate, '108 Mbps', 'tx rate converted'); eq(det.mode, '802.11n', 'mode carried'); eq(N.connectionDetail(null), null, 'no diag => null'); eq(N.connectionDetail('{"type":"a{sv}","data":[{}]}'), null, 'no ConnectedBss => null (disconnected)'); console.log(passed + ' assertions passed'); console.log('network.test.js: ALL PASS');