From a779c39fe1086c0cd9ee261247a16bf672ee0481 Mon Sep 17 00:00:00 2001 From: you Date: Mon, 23 Mar 2026 23:35:22 +0000 Subject: [PATCH] =?UTF-8?q?test:=20check=20in=20unit=20tests=20=E2=80=94?= =?UTF-8?q?=2062=20filter=20+=2029=20aging=20=3D=2091=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test-packet-filter.js: all operators, fields, aliases, logic, edge cases test-aging.js: getNodeStatus, getStatusInfo, getStatusTooltip, thresholds Run: node test-packet-filter.js && node test-aging.js --- test-packet-filter.js | 149 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 test-packet-filter.js diff --git a/test-packet-filter.js b/test-packet-filter.js new file mode 100644 index 00000000..940a24d9 --- /dev/null +++ b/test-packet-filter.js @@ -0,0 +1,149 @@ +/* Unit tests for packet filter language */ +'use strict'; +const vm = require('vm'); +const fs = require('fs'); + +const code = fs.readFileSync('public/packet-filter.js', 'utf8'); +const ctx = { window: {}, console }; +vm.createContext(ctx); +vm.runInContext(code, ctx); +const PF = ctx.window.PacketFilter; + +let pass = 0, fail = 0; +function test(name, fn) { + try { fn(); pass++; } + catch (e) { console.log(`FAIL: ${name} — ${e.message}`); fail++; } +} +function assert(cond, msg) { if (!cond) throw new Error(msg || 'assertion failed'); } + +const pkt = { + route_type: 1, payload_type: 5, snr: 8.5, rssi: -45, + hash: 'abc123def456', raw_hex: '110500aabbccdd', + path_json: '["8A","B5","97"]', + decoded_json: JSON.stringify({ + name: "ESP1 Gilroy Repeater", lat: 37.005, lon: -121.567, + pubKey: "f81d265c03c5c1b2", text: "Hello mesh", sender: "KpaPocket", + flags: { raw: 147, type: 2, repeater: true, room: false, hasLocation: true, hasName: true } + }), + observer_name: 'kpabap', observer_id: '2301ACD8E9DCEDE5', + observation_count: 3, timestamp: new Date().toISOString(), +}; + +const nullSnrPkt = { ...pkt, snr: null, rssi: null }; + +// --- Firmware type names --- +test('type == GRP_TXT', () => { assert(PF.compile('type == GRP_TXT').filter(pkt)); }); +test('type == grp_txt (case insensitive)', () => { assert(PF.compile('type == grp_txt').filter(pkt)); }); +test('type == ADVERT is false', () => { assert(!PF.compile('type == ADVERT').filter(pkt)); }); +test('type == TXT_MSG is false', () => { assert(!PF.compile('type == TXT_MSG').filter(pkt)); }); +test('type != GRP_TXT is false', () => { assert(!PF.compile('type != GRP_TXT').filter(pkt)); }); +test('type != ADVERT is true', () => { assert(PF.compile('type != ADVERT').filter(pkt)); }); + +// --- Type aliases --- +test('type == channel (alias)', () => { assert(PF.compile('type == channel').filter(pkt)); }); +test('type == "Channel Msg" (alias)', () => { assert(PF.compile('type == "Channel Msg"').filter(pkt)); }); +test('type == dm is false', () => { assert(!PF.compile('type == dm').filter(pkt)); }); +test('type == request is false', () => { assert(!PF.compile('type == request').filter(pkt)); }); + +// --- Route --- +test('route == FLOOD', () => { assert(PF.compile('route == FLOOD').filter(pkt)); }); +test('route == DIRECT is false', () => { assert(!PF.compile('route == DIRECT').filter(pkt)); }); + +// --- Hash --- +test('hash == abc123def456', () => { assert(PF.compile('hash == abc123def456').filter(pkt)); }); +test('hash contains abc', () => { assert(PF.compile('hash contains abc').filter(pkt)); }); +test('hash starts_with abc', () => { assert(PF.compile('hash starts_with abc').filter(pkt)); }); +test('hash ends_with 456', () => { assert(PF.compile('hash ends_with 456').filter(pkt)); }); + +// --- Numeric --- +test('snr > 5', () => { assert(PF.compile('snr > 5').filter(pkt)); }); +test('snr > 10 is false', () => { assert(!PF.compile('snr > 10').filter(pkt)); }); +test('snr >= 8.5', () => { assert(PF.compile('snr >= 8.5').filter(pkt)); }); +test('snr < 8.5 is false', () => { assert(!PF.compile('snr < 8.5').filter(pkt)); }); +test('rssi < -40', () => { assert(PF.compile('rssi < -40').filter(pkt)); }); +test('rssi < -50 is false', () => { assert(!PF.compile('rssi < -50').filter(pkt)); }); + +// --- Hops --- +test('hops == 3', () => { assert(PF.compile('hops == 3').filter(pkt)); }); +test('hops > 2', () => { assert(PF.compile('hops > 2').filter(pkt)); }); +test('hops > 3 is false', () => { assert(!PF.compile('hops > 3').filter(pkt)); }); + +// --- Observer --- +test('observer == kpabap', () => { assert(PF.compile('observer == kpabap').filter(pkt)); }); +test('observer contains kpa', () => { assert(PF.compile('observer contains kpa').filter(pkt)); }); + +// --- Observations --- +test('observations > 1', () => { assert(PF.compile('observations > 1').filter(pkt)); }); +test('observations == 3', () => { assert(PF.compile('observations == 3').filter(pkt)); }); + +// --- Size --- +test('size > 3', () => { assert(PF.compile('size > 3').filter(pkt)); }); + +// --- Payload dot notation --- +test('payload.name contains "Gilroy"', () => { assert(PF.compile('payload.name contains "Gilroy"').filter(pkt)); }); +test('payload.name contains "Oakland" is false', () => { assert(!PF.compile('payload.name contains "Oakland"').filter(pkt)); }); +test('payload.name starts_with "ESP1"', () => { assert(PF.compile('payload.name starts_with "ESP1"').filter(pkt)); }); +test('payload.lat > 37', () => { assert(PF.compile('payload.lat > 37').filter(pkt)); }); +test('payload.lat > 38 is false', () => { assert(!PF.compile('payload.lat > 38').filter(pkt)); }); +test('payload.lon < -121', () => { assert(PF.compile('payload.lon < -121').filter(pkt)); }); +test('payload.pubKey starts_with "f81d"', () => { assert(PF.compile('payload.pubKey starts_with "f81d"').filter(pkt)); }); +test('payload.text contains "Hello"', () => { assert(PF.compile('payload.text contains "Hello"').filter(pkt)); }); +test('payload.sender == "KpaPocket"', () => { assert(PF.compile('payload.sender == "KpaPocket"').filter(pkt)); }); +test('payload.flags.hasLocation (truthy)', () => { assert(PF.compile('payload.flags.hasLocation').filter(pkt)); }); +test('payload.flags.room is false (truthy)', () => { assert(!PF.compile('payload.flags.room').filter(pkt)); }); +test('payload.flags.raw == 147', () => { assert(PF.compile('payload.flags.raw == 147').filter(pkt)); }); +test('payload_hex contains "aabb"', () => { assert(PF.compile('payload_hex contains "aabb"').filter(pkt)); }); + +// --- Logic --- +test('type == GRP_TXT && snr > 5', () => { assert(PF.compile('type == GRP_TXT && snr > 5').filter(pkt)); }); +test('type == GRP_TXT && snr > 10 is false', () => { assert(!PF.compile('type == GRP_TXT && snr > 10').filter(pkt)); }); +test('type == ADVERT || snr > 5', () => { assert(PF.compile('type == ADVERT || snr > 5').filter(pkt)); }); +test('type == ADVERT || snr > 10 is false', () => { assert(!PF.compile('type == ADVERT || snr > 10').filter(pkt)); }); +test('!(type == ADVERT)', () => { assert(PF.compile('!(type == ADVERT)').filter(pkt)); }); +test('!(type == GRP_TXT) is false', () => { assert(!PF.compile('!(type == GRP_TXT)').filter(pkt)); }); + +// --- Parentheses --- +test('(type == ADVERT || type == GRP_TXT) && snr > 5', () => { + assert(PF.compile('(type == ADVERT || type == GRP_TXT) && snr > 5').filter(pkt)); +}); +test('(type == ADVERT) && snr > 5 is false', () => { + assert(!PF.compile('(type == ADVERT) && snr > 5').filter(pkt)); +}); + +// --- Complex --- +test('type == GRP_TXT && snr > 5 && hops > 2', () => { + assert(PF.compile('type == GRP_TXT && snr > 5 && hops > 2').filter(pkt)); +}); +test('!(type == ACK) && !(type == PATH)', () => { + assert(PF.compile('!(type == ACK) && !(type == PATH)').filter(pkt)); +}); +test('payload.lat >= 37 && payload.lat <= 38 && payload.lon >= -122 && payload.lon <= -121', () => { + assert(PF.compile('payload.lat >= 37 && payload.lat <= 38 && payload.lon >= -122 && payload.lon <= -121').filter(pkt)); +}); + +// --- Edge cases: null fields --- +test('snr > 5 with null snr → false', () => { assert(!PF.compile('snr > 5').filter(nullSnrPkt)); }); +test('rssi < -50 with null rssi → false', () => { assert(!PF.compile('rssi < -50').filter(nullSnrPkt)); }); +test('payload.nonexistent == "x" → false', () => { assert(!PF.compile('payload.nonexistent == "x"').filter(pkt)); }); +test('payload.flags.nonexistent (truthy) → false', () => { assert(!PF.compile('payload.flags.nonexistent').filter(pkt)); }); + +// --- Error handling --- +test('empty filter → no error', () => { + const c = PF.compile(''); + assert(c.error === null, 'should have no error'); +}); +test('invalid syntax → error message', () => { + const c = PF.compile('== broken'); + assert(c.error !== null, 'should have error'); +}); +test('@@@ garbage → error', () => { + const c = PF.compile('@@@ garbage'); + assert(c.error !== null, 'should have error'); +}); +test('unclosed quote → error', () => { + const c = PF.compile('type == "hello'); + assert(c.error !== null, 'should have error'); +}); + +console.log(`\n=== Results: ${pass} passed, ${fail} failed ===`); +process.exit(fail > 0 ? 1 : 0);