mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-03-29 08:29:55 +00:00
test: push server coverage from 76% to 88% (200 tests)
Added ~100 new tests covering: - WebSocket connection - All decoder payload types (REQ, RESPONSE, TXT_MSG, ACK, GRP_TXT, ANON_REQ, PATH, TRACE, UNKNOWN) - Short/malformed payload error branches - Transport route decoding - db.searchNodes, db.getNodeHealth, db.getNodeAnalytics direct calls - db.updateObserverStatus - Packet store: getById, getSiblings, getTimestamps, all, filter, getStats, queryGrouped, countForNode, findPacketsForNode, _transmissionsForObserver - Cache SWR (stale-while-revalidate), isStale, recompute, debouncedInvalidateAll - server-helpers: disambiguateHops (ambiguous, backward pass, distance unreliable, unknown prefix, no-coord), isHashSizeFlipFlop - Additional route query param branches (multi-filter, nocache, sortBy, region variants) - Channel message dedup/parsing with proper CHAN type data - Peer interaction coverage via sender_key/recipient_key in decoded_json - SPA fallback paths, perf/nocache bypass, hash-based packet lookup - Node health/analytics/paths for nodes with actual packet data Coverage: 88.38% statements, 78.95% branches, 94.59% functions
This commit is contained in:
@@ -6,7 +6,7 @@ process.env.NODE_ENV = 'test';
|
||||
process.env.SEED_DB = 'true'; // Seed test data
|
||||
|
||||
const request = require('supertest');
|
||||
const { app, pktStore, db, cache } = require('./server');
|
||||
const { app, server, wss, pktStore, db, cache } = require('./server');
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
|
||||
@@ -99,6 +99,17 @@ function seedTestData() {
|
||||
try { pktStore.insert(chanPkt); } catch {}
|
||||
try { db.insertTransmission(chanPkt); } catch {}
|
||||
|
||||
// Another CHAN message for dedup code path coverage
|
||||
const chanPkt3 = {
|
||||
raw_hex: 'FF00EE00DD00CC00',
|
||||
timestamp: now, observer_id: 'test-obs-1', observer_name: 'TestObs', snr: 7, rssi: -75,
|
||||
hash: 'test-hash-006', route_type: 1, payload_type: 5, payload_version: 1,
|
||||
path_json: JSON.stringify([]),
|
||||
decoded_json: JSON.stringify({ type: 'CHAN', channel: 'ch01', text: 'UserB: Test msg', sender: 'UserB' }),
|
||||
};
|
||||
try { pktStore.insert(chanPkt3); } catch {}
|
||||
try { db.insertTransmission(chanPkt3); } catch {}
|
||||
|
||||
// Duplicate of same message from different observer (for dedup/repeats coverage)
|
||||
const chanPkt2 = {
|
||||
raw_hex: 'AA00BB00CC00DD00',
|
||||
@@ -110,6 +121,17 @@ function seedTestData() {
|
||||
try { pktStore.insert(chanPkt2); } catch {}
|
||||
try { db.insertTransmission(chanPkt2); } catch {}
|
||||
|
||||
// Packet with sender_key/recipient_key for peer interaction coverage in db.getNodeAnalytics
|
||||
const peerPkt = {
|
||||
raw_hex: 'DEADBEEF00112233',
|
||||
timestamp: yesterday, observer_id: 'test-obs-1', observer_name: 'TestObs', snr: 8, rssi: -82,
|
||||
hash: 'test-hash-007', route_type: 0, payload_type: 2, payload_version: 1,
|
||||
path_json: JSON.stringify(['aabb']),
|
||||
decoded_json: JSON.stringify({ type: 'TXT_MSG', sender_key: 'aabb' + '0'.repeat(60), sender_name: 'TestRepeater1', recipient_key: 'ccdd' + '0'.repeat(60), recipient_name: 'TestRoom1', text: 'hello' }),
|
||||
};
|
||||
try { pktStore.insert(peerPkt); } catch {}
|
||||
try { db.insertTransmission(peerPkt); } catch {}
|
||||
|
||||
// Clear cache so fresh data is picked up
|
||||
cache.clear();
|
||||
}
|
||||
@@ -602,6 +624,420 @@ seedTestData();
|
||||
await request(app).get('/api/analytics/topology').expect(200);
|
||||
});
|
||||
|
||||
// ── WebSocket tests ──
|
||||
await t('WebSocket connection', async () => {
|
||||
const WebSocket = require('ws');
|
||||
await new Promise((resolve) => {
|
||||
if (server.address()) return resolve();
|
||||
server.listen(0, resolve);
|
||||
});
|
||||
const port = server.address().port;
|
||||
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
|
||||
await new Promise((resolve, reject) => {
|
||||
ws.on('open', resolve);
|
||||
ws.on('error', reject);
|
||||
setTimeout(() => reject(new Error('WS timeout')), 3000);
|
||||
});
|
||||
assert(ws.readyState === WebSocket.OPEN, 'WS should be open');
|
||||
ws.close();
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
});
|
||||
|
||||
// ── Additional query parameter branches ──
|
||||
await t('GET /api/nodes?sortBy=lastSeen', async () => {
|
||||
await request(app).get('/api/nodes?sortBy=lastSeen').expect(200);
|
||||
});
|
||||
await t('GET /api/nodes multi-filter', async () => {
|
||||
await request(app).get('/api/nodes?role=repeater®ion=SFO&lastHeard=86400&search=Test').expect(200);
|
||||
});
|
||||
await t('GET /api/packets?type=4', async () => {
|
||||
await request(app).get('/api/packets?type=4').expect(200);
|
||||
});
|
||||
await t('GET /api/packets multi-filter', async () => {
|
||||
await request(app).get('/api/packets?type=5&route=1&observer=test-obs-1').expect(200);
|
||||
});
|
||||
await t('GET /api/packets?node=TestRepeater1', async () => {
|
||||
await request(app).get('/api/packets?node=TestRepeater1').expect(200);
|
||||
});
|
||||
await t('GET /api/packets?groupByHash=true&type=4&observer=test-obs-1', async () => {
|
||||
await request(app).get('/api/packets?groupByHash=true&type=4&observer=test-obs-1').expect(200);
|
||||
});
|
||||
await t('GET /api/packets?groupByHash=true®ion=SFO', async () => {
|
||||
await request(app).get('/api/packets?groupByHash=true®ion=SFO').expect(200);
|
||||
});
|
||||
await t('GET /api/nodes?sortBy=name&search=nonexistent', async () => {
|
||||
const r = await request(app).get('/api/nodes?sortBy=name&search=nonexistent').expect(200);
|
||||
assert(r.body.nodes.length === 0, 'no nodes');
|
||||
});
|
||||
await t('GET /api/nodes?before=2000-01-01', async () => {
|
||||
const r = await request(app).get('/api/nodes?before=2000-01-01T00:00:00Z').expect(200);
|
||||
assert(r.body.nodes.length === 0, 'no nodes');
|
||||
});
|
||||
|
||||
// ── Node health/analytics/paths for nodes WITH packets ──
|
||||
const testRepeaterKey = 'aabb' + '0'.repeat(60);
|
||||
|
||||
await t('GET /api/nodes/:pubkey/health with packets', async () => {
|
||||
cache.clear();
|
||||
const r = await request(app).get(`/api/nodes/${testRepeaterKey}/health`).expect(200);
|
||||
assert(r.body.node && r.body.stats, 'should have node+stats');
|
||||
});
|
||||
await t('GET /api/nodes/:pubkey/analytics?days=30', async () => {
|
||||
cache.clear();
|
||||
const r = await request(app).get(`/api/nodes/${testRepeaterKey}/analytics?days=30`).expect(200);
|
||||
assert(r.body.computedStats, 'should have computedStats');
|
||||
});
|
||||
await t('GET /api/nodes/:pubkey/analytics?days=1', async () => {
|
||||
cache.clear();
|
||||
await request(app).get(`/api/nodes/${testRepeaterKey}/analytics?days=1`).expect(200);
|
||||
});
|
||||
await t('GET /api/nodes/:pubkey/paths with packets', async () => {
|
||||
cache.clear();
|
||||
const r = await request(app).get(`/api/nodes/${testRepeaterKey}/paths`).expect(200);
|
||||
assert(r.body.paths !== undefined, 'should have paths');
|
||||
});
|
||||
await t('GET /api/nodes/:pubkey existing', async () => {
|
||||
await request(app).get(`/api/nodes/${testRepeaterKey}`).expect(200);
|
||||
});
|
||||
await t('GET /api/nodes/:pubkey 404', async () => {
|
||||
await request(app).get('/api/nodes/' + '0'.repeat(63) + '1').expect(404);
|
||||
});
|
||||
|
||||
// ── Observer analytics ──
|
||||
await t('GET /api/observers/test-obs-1/analytics', async () => {
|
||||
cache.clear();
|
||||
const r = await request(app).get('/api/observers/test-obs-1/analytics').expect(200);
|
||||
assert(r.body.timeline !== undefined, 'should have timeline');
|
||||
});
|
||||
await t('GET /api/observers/test-obs-1/analytics?days=1', async () => {
|
||||
cache.clear();
|
||||
await request(app).get('/api/observers/test-obs-1/analytics?days=1').expect(200);
|
||||
});
|
||||
|
||||
// ── Traces ──
|
||||
await t('GET /api/traces/:hash existing', async () => {
|
||||
const r = await request(app).get('/api/traces/test-hash-001').expect(200);
|
||||
assert(r.body.traces, 'should have traces');
|
||||
});
|
||||
|
||||
// ── Resolve hops ──
|
||||
await t('GET /api/resolve-hops?hops=aabb', async () => {
|
||||
await request(app).get('/api/resolve-hops?hops=aabb').expect(200);
|
||||
});
|
||||
await t('GET /api/resolve-hops?hops=ffff', async () => {
|
||||
await request(app).get('/api/resolve-hops?hops=ffff').expect(200);
|
||||
});
|
||||
|
||||
// ── Analytics with nonexistent region ──
|
||||
for (const ep of ['rf', 'topology', 'channels', 'distance', 'hash-sizes']) {
|
||||
await t(`GET /api/analytics/${ep}?region=ZZZZ`, async () => {
|
||||
cache.clear();
|
||||
await request(app).get(`/api/analytics/${ep}?region=ZZZZ`).expect(200);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Subpath endpoints ──
|
||||
await t('GET /api/analytics/subpaths?minLen=2&maxLen=2', async () => {
|
||||
cache.clear();
|
||||
await request(app).get('/api/analytics/subpaths?minLen=2&maxLen=2&limit=5').expect(200);
|
||||
});
|
||||
await t('GET /api/analytics/subpath-detail?hops=aabb,ccdd,eeff', async () => {
|
||||
cache.clear();
|
||||
await request(app).get('/api/analytics/subpath-detail?hops=aabb,ccdd,eeff').expect(200);
|
||||
});
|
||||
|
||||
// ── POST /api/packets with observer+region ──
|
||||
await t('POST /api/packets with observer+region', async () => {
|
||||
const r = await request(app).post('/api/packets')
|
||||
.send({ hex: '11451000D818206D3AAC152C8A91F89957E6D30CA51F36E28790228971C473B755F244F718754CF5EE4A2FD58D944466E42CDED140C66D0CC590183E32BAF40F112BE8F3F2BDF6012B4B2793C52F1D36F69EE054D9A05593286F78453E56C0EC4A3EB95DDA2A7543FCCC00B939CACC009278603902FC12BCF84B706120526F6F6620536F6C6172', observer: 'test-api-obs', region: 'LAX', snr: 12, rssi: -75 });
|
||||
assert([200, 400, 403].includes(r.status), 'should handle');
|
||||
});
|
||||
|
||||
// ── POST /api/decode with whitespace ──
|
||||
await t('POST /api/decode whitespace hex', async () => {
|
||||
const r = await request(app).post('/api/decode').send({ hex: ' 1145 1000 D818 206D ' });
|
||||
assert([200, 400].includes(r.status), 'should handle');
|
||||
});
|
||||
|
||||
// ── Direct db function calls ──
|
||||
await t('db.searchNodes', () => {
|
||||
assert(Array.isArray(db.searchNodes('Test', 10)), 'should return array');
|
||||
assert(db.searchNodes('zzzznonexistent', 5).length === 0, 'empty for no match');
|
||||
});
|
||||
await t('db.getNodeHealth existing', () => {
|
||||
const h = db.getNodeHealth(testRepeaterKey);
|
||||
assert(h && h.node && h.stats, 'should have node+stats');
|
||||
});
|
||||
await t('db.getNodeHealth nonexistent', () => {
|
||||
assert(db.getNodeHealth('nonexistent') === null, 'null for missing');
|
||||
});
|
||||
await t('db.getNodeAnalytics existing', () => {
|
||||
const a = db.getNodeAnalytics(testRepeaterKey, 7);
|
||||
assert(a && a.computedStats, 'should have computedStats');
|
||||
});
|
||||
await t('db.getNodeAnalytics nonexistent', () => {
|
||||
assert(db.getNodeAnalytics('nonexistent', 7) === null, 'null for missing');
|
||||
});
|
||||
await t('db.getNodeAnalytics for named node', () => {
|
||||
const a = db.getNodeAnalytics('ccdd' + '0'.repeat(60), 30);
|
||||
assert(a !== null, 'should return analytics');
|
||||
});
|
||||
await t('db.getNodeHealth for named node', () => {
|
||||
assert(db.getNodeHealth('ccdd' + '0'.repeat(60)) !== null, 'should have health');
|
||||
});
|
||||
await t('db.updateObserverStatus', () => {
|
||||
db.updateObserverStatus({ id: 'test-status-obs', name: 'StatusObs', iata: 'LAX', model: 'test', firmware: '1.0', client_version: '2.0', radio: '915,125,7,5', battery_mv: 3700, uptime_secs: 86400, noise_floor: -120 });
|
||||
});
|
||||
|
||||
// ── Packet store direct methods ──
|
||||
await t('pktStore.getById missing', () => { assert(pktStore.getById(999999) === null); });
|
||||
await t('pktStore.getSiblings missing', () => { assert(pktStore.getSiblings('nonexistent').length === 0); });
|
||||
await t('pktStore.getTimestamps', () => { assert(Array.isArray(pktStore.getTimestamps('2000-01-01T00:00:00Z'))); });
|
||||
await t('pktStore.all', () => { assert(Array.isArray(pktStore.all())); });
|
||||
await t('pktStore.filter', () => { assert(Array.isArray(pktStore.filter(p => p.payload_type === 4))); });
|
||||
await t('pktStore.getStats', () => { const s = pktStore.getStats(); assert(s.inMemory !== undefined && s.indexes); });
|
||||
await t('pktStore.queryGrouped', () => { assert(pktStore.queryGrouped({ limit: 5, type: 4 }).packets !== undefined); });
|
||||
await t('pktStore.queryGrouped region+since', () => { assert(pktStore.queryGrouped({ limit: 5, region: 'SFO', since: '2000-01-01' }).packets !== undefined); });
|
||||
await t('pktStore.countForNode existing', () => { assert(pktStore.countForNode(testRepeaterKey).transmissions !== undefined); });
|
||||
await t('pktStore.countForNode missing', () => { assert(pktStore.countForNode('nonexistent').transmissions === 0); });
|
||||
await t('pktStore.findPacketsForNode', () => { assert(pktStore.findPacketsForNode(testRepeaterKey).packets !== undefined); });
|
||||
await t('pktStore._transmissionsForObserver with fromTx', () => {
|
||||
assert(Array.isArray(pktStore._transmissionsForObserver('test-obs-1', pktStore.all())));
|
||||
});
|
||||
|
||||
// ── Cache SWR (stale-while-revalidate) ──
|
||||
await t('Cache stale-while-revalidate', async () => {
|
||||
// 50ms TTL, grace period = 100ms
|
||||
cache.set('swr-test', { data: 42 }, 50);
|
||||
await new Promise(r => setTimeout(r, 60)); // Wait past expiry but within grace
|
||||
const stale = cache.get('swr-test');
|
||||
assert(stale && stale.data === 42, 'should return stale value within grace');
|
||||
});
|
||||
await t('Cache fully expired (past grace)', async () => {
|
||||
cache.set('swr-expired', { data: 99 }, 10);
|
||||
await new Promise(r => setTimeout(r, 30)); // Past 2× TTL
|
||||
const expired = cache.get('swr-expired');
|
||||
assert(expired === undefined, 'should return undefined past grace');
|
||||
});
|
||||
await t('Cache isStale', async () => {
|
||||
cache.set('stale-test', { data: 1 }, 50);
|
||||
await new Promise(r => setTimeout(r, 60));
|
||||
assert(cache.isStale('stale-test') === true, 'should be stale');
|
||||
});
|
||||
await t('Cache recompute', () => {
|
||||
let ran = false;
|
||||
cache.recompute('recompute-test', () => { ran = true; });
|
||||
assert(ran === true, 'recompute fn should run');
|
||||
});
|
||||
await t('Cache debouncedInvalidateAll', async () => {
|
||||
cache.debouncedInvalidateAll();
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
});
|
||||
|
||||
// ── Cache operations ──
|
||||
await t('Cache set/get/invalidate', () => {
|
||||
cache.set('test-key', { data: 1 }, 60000);
|
||||
assert(cache.get('test-key').data === 1);
|
||||
cache.invalidate('test-key');
|
||||
});
|
||||
await t('Cache invalidate+refetch', async () => {
|
||||
cache.clear();
|
||||
await request(app).get('/api/stats').expect(200);
|
||||
await request(app).get('/api/stats').expect(200);
|
||||
});
|
||||
|
||||
// ── Channel messages fresh (dedup coverage) ──
|
||||
await t('GET /api/channels/ch01/messages fresh', async () => {
|
||||
cache.clear();
|
||||
const r = await request(app).get('/api/channels/ch01/messages').expect(200);
|
||||
assert(r.body.messages !== undefined, 'should have messages');
|
||||
});
|
||||
await t('GET /api/channels/ch01/messages?limit=1&offset=0', async () => {
|
||||
cache.clear();
|
||||
await request(app).get('/api/channels/ch01/messages?limit=1&offset=0').expect(200);
|
||||
});
|
||||
|
||||
// ── Multi-filter packet queries ──
|
||||
await t('GET /api/packets?type=4&node=TestRepeater1', async () => {
|
||||
await request(app).get('/api/packets?type=4&node=TestRepeater1').expect(200);
|
||||
});
|
||||
await t('GET /api/packets all filters', async () => {
|
||||
await request(app).get('/api/packets?type=4&route=1&since=2000-01-01T00:00:00Z&until=2099-01-01T00:00:00Z&observer=test-obs-1&hash=test-hash-001').expect(200);
|
||||
});
|
||||
await t('GET /api/packets?type=5®ion=SFO&node=TestRepeater1', async () => {
|
||||
await request(app).get('/api/packets?type=5®ion=SFO&node=TestRepeater1&since=2000-01-01T00:00:00Z').expect(200);
|
||||
});
|
||||
|
||||
// ── Perf/nocache bypass ──
|
||||
await t('GET /api/stats?nocache=1', async () => {
|
||||
await request(app).get('/api/stats?nocache=1').expect(200);
|
||||
});
|
||||
await t('GET /api/nodes?nocache=1', async () => {
|
||||
await request(app).get('/api/nodes?nocache=1').expect(200);
|
||||
});
|
||||
|
||||
// ── More route branch coverage ──
|
||||
await t('GET /api/packets/:id by hash', async () => {
|
||||
await request(app).get('/api/packets/testhash00000001').expect(404); // 16 hex chars
|
||||
});
|
||||
await t('GET /api/packets/:id by string non-hash', async () => {
|
||||
await request(app).get('/api/packets/not-a-hash-or-number').expect(404);
|
||||
});
|
||||
|
||||
// ── SPA fallback paths ──
|
||||
for (const path of ['/map', '/packets', '/analytics', '/live']) {
|
||||
await t(`GET ${path} SPA fallback`, async () => {
|
||||
const r = await request(app).get(path);
|
||||
assert([200, 304, 404].includes(r.status));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Network status ──
|
||||
await t('GET /api/nodes/network-status?region=ZZZZ', async () => {
|
||||
cache.clear();
|
||||
await request(app).get('/api/nodes/network-status?region=ZZZZ').expect(200);
|
||||
});
|
||||
|
||||
// ── Bulk health variants ──
|
||||
await t('GET /api/nodes/bulk-health?limit=2', async () => {
|
||||
cache.clear();
|
||||
await request(app).get('/api/nodes/bulk-health?limit=2').expect(200);
|
||||
});
|
||||
await t('GET /api/nodes/bulk-health?region=NYC', async () => {
|
||||
cache.clear();
|
||||
await request(app).get('/api/nodes/bulk-health?region=NYC').expect(200);
|
||||
});
|
||||
|
||||
// ── Decoder: various payload types ──
|
||||
await t('POST /api/decode REQ', async () => {
|
||||
const r = await request(app).post('/api/decode').send({ hex: '0000' + 'AA'.repeat(20) });
|
||||
assert(r.status === 200 && r.body.decoded.payload.type === 'REQ');
|
||||
});
|
||||
await t('POST /api/decode RESPONSE', async () => {
|
||||
const r = await request(app).post('/api/decode').send({ hex: '0400' + 'AA'.repeat(20) });
|
||||
assert(r.status === 200 && r.body.decoded.payload.type === 'RESPONSE');
|
||||
});
|
||||
await t('POST /api/decode TXT_MSG', async () => {
|
||||
const r = await request(app).post('/api/decode').send({ hex: '0800' + 'AA'.repeat(20) });
|
||||
assert(r.status === 200 && r.body.decoded.payload.type === 'TXT_MSG');
|
||||
});
|
||||
await t('POST /api/decode ACK', async () => {
|
||||
const r = await request(app).post('/api/decode').send({ hex: '0C00' + 'BB'.repeat(6) });
|
||||
assert(r.status === 200 && r.body.decoded.payload.type === 'ACK');
|
||||
});
|
||||
await t('POST /api/decode GRP_TXT', async () => {
|
||||
const r = await request(app).post('/api/decode').send({ hex: '1500' + 'CC'.repeat(10) });
|
||||
assert(r.status === 200 && r.body.decoded.payload.type === 'GRP_TXT');
|
||||
});
|
||||
await t('POST /api/decode ANON_REQ', async () => {
|
||||
const r = await request(app).post('/api/decode').send({ hex: '1D00' + 'DD'.repeat(40) });
|
||||
assert(r.status === 200 && r.body.decoded.payload.type === 'ANON_REQ');
|
||||
});
|
||||
await t('POST /api/decode PATH', async () => {
|
||||
const r = await request(app).post('/api/decode').send({ hex: '2100' + 'EE'.repeat(10) });
|
||||
assert(r.status === 200 && r.body.decoded.payload.type === 'PATH');
|
||||
});
|
||||
await t('POST /api/decode TRACE', async () => {
|
||||
const r = await request(app).post('/api/decode').send({ hex: '2500' + 'FF'.repeat(12) });
|
||||
assert(r.status === 200 && r.body.decoded.payload.type === 'TRACE');
|
||||
});
|
||||
await t('POST /api/decode UNKNOWN type', async () => {
|
||||
const r = await request(app).post('/api/decode').send({ hex: '3C00' + '00'.repeat(10) });
|
||||
assert(r.status === 200 && r.body.decoded.payload.type === 'UNKNOWN');
|
||||
});
|
||||
await t('POST /api/decode TRANSPORT_FLOOD', async () => {
|
||||
const r = await request(app).post('/api/decode').send({ hex: '1200AABBCCDD' + '00'.repeat(101) });
|
||||
assert([200, 400].includes(r.status));
|
||||
});
|
||||
await t('POST /api/decode minimal ADVERT', async () => {
|
||||
await request(app).post('/api/decode').send({ hex: '1100' + '00'.repeat(100) }).expect(200);
|
||||
});
|
||||
await t('POST /api/decode too-short', async () => {
|
||||
await request(app).post('/api/decode').send({ hex: 'AA' }).expect(400);
|
||||
});
|
||||
// Short payloads triggering error branches
|
||||
await t('POST /api/decode GRP_TXT too short', async () => {
|
||||
await request(app).post('/api/decode').send({ hex: '1500AABB' }).expect(200);
|
||||
});
|
||||
await t('POST /api/decode ADVERT too short', async () => {
|
||||
await request(app).post('/api/decode').send({ hex: '1100' + 'AA'.repeat(10) }).expect(200);
|
||||
});
|
||||
await t('POST /api/decode TRACE too short', async () => {
|
||||
await request(app).post('/api/decode').send({ hex: '2500' + 'FF'.repeat(5) }).expect(200);
|
||||
});
|
||||
await t('POST /api/decode PATH too short', async () => {
|
||||
await request(app).post('/api/decode').send({ hex: '2100' + 'EE'.repeat(2) }).expect(200);
|
||||
});
|
||||
await t('POST /api/decode ANON_REQ too short', async () => {
|
||||
await request(app).post('/api/decode').send({ hex: '1D00' + 'DD'.repeat(10) }).expect(200);
|
||||
});
|
||||
|
||||
// ── server-helpers: disambiguateHops direct tests ──
|
||||
await t('disambiguateHops ambiguous multi-candidate', () => {
|
||||
const helpers = require('./server-helpers');
|
||||
const allNodes = [
|
||||
{ public_key: 'aabb' + '0'.repeat(60), name: 'Node1', lat: 37.7, lon: -122.4 },
|
||||
{ public_key: 'aabb' + '1'.repeat(60), name: 'Node2', lat: 34.0, lon: -118.2 },
|
||||
{ public_key: 'ccdd' + '0'.repeat(60), name: 'Node3', lat: 40.7, lon: -74.0 },
|
||||
];
|
||||
const resolved = helpers.disambiguateHops(['aabb', 'ccdd'], allNodes);
|
||||
assert(resolved.length === 2 && resolved[0].name);
|
||||
});
|
||||
await t('disambiguateHops backward pass', () => {
|
||||
const helpers = require('./server-helpers');
|
||||
// Ambiguous first hop, known second → backward pass resolves first
|
||||
const allNodes = [
|
||||
{ public_key: 'aa' + '3'.repeat(62), name: 'ANode1', lat: 37.7, lon: -122.4 },
|
||||
{ public_key: 'aa' + '4'.repeat(62), name: 'ANode2', lat: 34.0, lon: -118.2 },
|
||||
{ public_key: 'bb' + '3'.repeat(62), name: 'BNode', lat: 40.7, lon: -74.0 },
|
||||
];
|
||||
const resolved = helpers.disambiguateHops(['aa', 'bb'], allNodes);
|
||||
assert(resolved.length === 2);
|
||||
});
|
||||
await t('disambiguateHops distance unreliable', () => {
|
||||
const helpers = require('./server-helpers');
|
||||
const allNodes = [
|
||||
{ public_key: 'aa' + '5'.repeat(62), name: 'Near1', lat: 37.7, lon: -122.4 },
|
||||
{ public_key: 'bb' + '5'.repeat(62), name: 'FarAway', lat: -33.8, lon: 151.2 },
|
||||
{ public_key: 'cc' + '5'.repeat(62), name: 'Near2', lat: 37.8, lon: -122.3 },
|
||||
];
|
||||
const resolved = helpers.disambiguateHops(['aa', 'bb', 'cc'], allNodes, 0.5);
|
||||
assert(resolved[1].unreliable === true, 'middle node should be unreliable');
|
||||
});
|
||||
await t('disambiguateHops unknown prefix', () => {
|
||||
const helpers = require('./server-helpers');
|
||||
const allNodes = [{ public_key: 'aabb' + '0'.repeat(60), name: 'Node1', lat: 37.7, lon: -122.4 }];
|
||||
const resolved = helpers.disambiguateHops(['ffff', 'aabb'], allNodes);
|
||||
assert(resolved[0].known === false);
|
||||
});
|
||||
await t('disambiguateHops single known match', () => {
|
||||
const helpers = require('./server-helpers');
|
||||
const allNodes = [{ public_key: 'ccdd' + '6'.repeat(60), name: 'UniqueNode', lat: 40.7, lon: -74.0 }];
|
||||
const resolved = helpers.disambiguateHops(['ccdd'], allNodes);
|
||||
assert(resolved[0].known === true && resolved[0].name === 'UniqueNode');
|
||||
});
|
||||
await t('disambiguateHops no-coord node', () => {
|
||||
const helpers = require('./server-helpers');
|
||||
const allNodes = [{ public_key: 'aabb' + '7'.repeat(60), name: 'NoCoord', lat: 0, lon: 0 }];
|
||||
const resolved = helpers.disambiguateHops(['aabb'], allNodes);
|
||||
assert(resolved.length === 1);
|
||||
});
|
||||
|
||||
// ── isHashSizeFlipFlop ──
|
||||
await t('isHashSizeFlipFlop true', () => {
|
||||
const h = require('./server-helpers');
|
||||
assert(h.isHashSizeFlipFlop([1, 2, 1, 2], new Set([1, 2])) === true);
|
||||
});
|
||||
await t('isHashSizeFlipFlop false stable', () => {
|
||||
const h = require('./server-helpers');
|
||||
assert(h.isHashSizeFlipFlop([1, 1, 1], new Set([1])) === false);
|
||||
});
|
||||
await t('isHashSizeFlipFlop false short/null', () => {
|
||||
const h = require('./server-helpers');
|
||||
assert(h.isHashSizeFlipFlop([1, 2], new Set([1, 2])) === false);
|
||||
assert(h.isHashSizeFlipFlop(null, null) === false);
|
||||
});
|
||||
|
||||
// ── Summary ──
|
||||
console.log(`\n═══ Server Route Tests: ${passed} passed, ${failed} failed ═══`);
|
||||
if (failed > 0) process.exit(1);
|
||||
|
||||
Reference in New Issue
Block a user