Files
meshcore-analyzer/test-decoder.js
Kpa-clawbot bcf7159538 fix: detect garbage text after channel decryption, fixes #197
After decryption produces text, validate it's printable UTF-8.
If it contains more than 2 non-printable characters (excluding
newline/tab), mark as decryption_failed with text: null.

Applied to both Node (decoder.js) and Go (cmd/ingestor/decoder.go)
decoders. Added tests for garbage and valid text in both.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-27 21:48:37 -07:00

631 lines
22 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Unit tests for decoder.js */
'use strict';
const assert = require('assert');
const { decodePacket, validateAdvert, ROUTE_TYPES, PAYLOAD_TYPES, VALID_ROLES } = require('./decoder');
let passed = 0, failed = 0;
function test(name, fn) {
try { fn(); passed++; console.log(`${name}`); }
catch (e) { failed++; console.log(`${name}: ${e.message}`); }
}
// === Constants ===
console.log('\n=== Constants ===');
test('ROUTE_TYPES has 4 entries', () => assert.strictEqual(Object.keys(ROUTE_TYPES).length, 4));
test('PAYLOAD_TYPES has 13 entries', () => assert.strictEqual(Object.keys(PAYLOAD_TYPES).length, 13));
test('VALID_ROLES has repeater, companion, room, sensor', () => {
for (const r of ['repeater', 'companion', 'room', 'sensor']) assert(VALID_ROLES.has(r));
});
// === Header decoding ===
console.log('\n=== Header decoding ===');
test('FLOOD + ADVERT = 0x11', () => {
const p = decodePacket('1100' + '00'.repeat(101));
assert.strictEqual(p.header.routeType, 1);
assert.strictEqual(p.header.routeTypeName, 'FLOOD');
assert.strictEqual(p.header.payloadType, 4);
assert.strictEqual(p.header.payloadTypeName, 'ADVERT');
});
test('TRANSPORT_FLOOD = routeType 0', () => {
// 0x00 = TRANSPORT_FLOOD + REQ(0), needs transport codes + 16 byte payload
const hex = '0000' + 'AABB' + 'CCDD' + '00'.repeat(16);
const p = decodePacket(hex);
assert.strictEqual(p.header.routeType, 0);
assert.strictEqual(p.header.routeTypeName, 'TRANSPORT_FLOOD');
assert.notStrictEqual(p.transportCodes, null);
assert.strictEqual(p.transportCodes.nextHop, 'AABB');
assert.strictEqual(p.transportCodes.lastHop, 'CCDD');
});
test('TRANSPORT_DIRECT = routeType 3', () => {
const hex = '0300' + '1122' + '3344' + '00'.repeat(16);
const p = decodePacket(hex);
assert.strictEqual(p.header.routeType, 3);
assert.strictEqual(p.header.routeTypeName, 'TRANSPORT_DIRECT');
assert.strictEqual(p.transportCodes.nextHop, '1122');
});
test('DIRECT = routeType 2, no transport codes', () => {
const hex = '0200' + '00'.repeat(16);
const p = decodePacket(hex);
assert.strictEqual(p.header.routeType, 2);
assert.strictEqual(p.header.routeTypeName, 'DIRECT');
assert.strictEqual(p.transportCodes, null);
});
test('payload version extracted', () => {
// 0xC1 = 11_0000_01 → version=3, payloadType=0, routeType=1
const hex = 'C100' + '00'.repeat(16);
const p = decodePacket(hex);
assert.strictEqual(p.header.payloadVersion, 3);
});
// === Path decoding ===
console.log('\n=== Path decoding ===');
test('hashSize=1, hashCount=3', () => {
// pathByte = 0x03 → (0>>6)+1=1, 3&0x3F=3
const hex = '1103' + 'AABBCC' + '00'.repeat(101);
const p = decodePacket(hex);
assert.strictEqual(p.path.hashSize, 1);
assert.strictEqual(p.path.hashCount, 3);
assert.strictEqual(p.path.hops.length, 3);
assert.strictEqual(p.path.hops[0], 'AA');
assert.strictEqual(p.path.hops[1], 'BB');
assert.strictEqual(p.path.hops[2], 'CC');
});
test('hashSize=2, hashCount=2', () => {
// pathByte = 0x42 → (1>>0=1)+1=2, 2&0x3F=2
const hex = '1142' + 'AABB' + 'CCDD' + '00'.repeat(101);
const p = decodePacket(hex);
assert.strictEqual(p.path.hashSize, 2);
assert.strictEqual(p.path.hashCount, 2);
assert.strictEqual(p.path.hops[0], 'AABB');
assert.strictEqual(p.path.hops[1], 'CCDD');
});
test('hashSize=4 from pathByte 0xC1', () => {
// 0xC1 = 11_000001 → hashSize=(3)+1=4, hashCount=1
const hex = '11C1' + 'DEADBEEF' + '00'.repeat(101);
const p = decodePacket(hex);
assert.strictEqual(p.path.hashSize, 4);
assert.strictEqual(p.path.hashCount, 1);
assert.strictEqual(p.path.hops[0], 'DEADBEEF');
});
test('zero hops', () => {
const hex = '1100' + '00'.repeat(101);
const p = decodePacket(hex);
assert.strictEqual(p.path.hashCount, 0);
assert.strictEqual(p.path.hops.length, 0);
});
// === Payload types ===
console.log('\n=== ADVERT payload ===');
test('ADVERT with name and location', () => {
const pkt = decodePacket(
'11451000D818206D3AAC152C8A91F89957E6D30CA51F36E28790228971C473B755F244F718754CF5EE4A2FD58D944466E42CDED140C66D0CC590183E32BAF40F112BE8F3F2BDF6012B4B2793C52F1D36F69EE054D9A05593286F78453E56C0EC4A3EB95DDA2A7543FCCC00B939CACC009278603902FC12BCF84B706120526F6F6620536F6C6172'
);
assert.strictEqual(pkt.payload.type, 'ADVERT');
assert.strictEqual(pkt.payload.name, 'Kpa Roof Solar');
assert(pkt.payload.pubKey.length === 64);
assert(pkt.payload.timestamp > 0);
assert(pkt.payload.timestampISO);
assert(pkt.payload.signature.length === 128);
});
test('ADVERT flags: chat type=1', () => {
const pubKey = 'AB'.repeat(32);
const ts = '01000000';
const sig = 'CC'.repeat(64);
const flags = '01'; // type=1 → chat
const hex = '1100' + pubKey + ts + sig + flags;
const p = decodePacket(hex);
assert.strictEqual(p.payload.flags.type, 1);
assert.strictEqual(p.payload.flags.chat, true);
assert.strictEqual(p.payload.flags.repeater, false);
});
test('ADVERT flags: repeater type=2', () => {
const pubKey = 'AB'.repeat(32);
const ts = '01000000';
const sig = 'CC'.repeat(64);
const flags = '02';
const hex = '1100' + pubKey + ts + sig + flags;
const p = decodePacket(hex);
assert.strictEqual(p.payload.flags.type, 2);
assert.strictEqual(p.payload.flags.repeater, true);
});
test('ADVERT flags: room type=3', () => {
const pubKey = 'AB'.repeat(32);
const ts = '01000000';
const sig = 'CC'.repeat(64);
const flags = '03';
const hex = '1100' + pubKey + ts + sig + flags;
const p = decodePacket(hex);
assert.strictEqual(p.payload.flags.type, 3);
assert.strictEqual(p.payload.flags.room, true);
});
test('ADVERT flags: sensor type=4', () => {
const pubKey = 'AB'.repeat(32);
const ts = '01000000';
const sig = 'CC'.repeat(64);
const flags = '04';
const hex = '1100' + pubKey + ts + sig + flags;
const p = decodePacket(hex);
assert.strictEqual(p.payload.flags.type, 4);
assert.strictEqual(p.payload.flags.sensor, true);
});
test('ADVERT flags: hasLocation', () => {
const pubKey = 'AB'.repeat(32);
const ts = '01000000';
const sig = 'CC'.repeat(64);
// flags=0x12 → type=2(repeater), hasLocation=true
const flags = '12';
const lat = '40420f00'; // 1000000 → 1.0 degrees
const lon = '80841e00'; // 2000000 → 2.0 degrees
const hex = '1100' + pubKey + ts + sig + flags + lat + lon;
const p = decodePacket(hex);
assert.strictEqual(p.payload.flags.hasLocation, true);
assert.strictEqual(p.payload.lat, 1.0);
assert.strictEqual(p.payload.lon, 2.0);
});
test('ADVERT flags: hasName', () => {
const pubKey = 'AB'.repeat(32);
const ts = '01000000';
const sig = 'CC'.repeat(64);
// flags=0x82 → type=2(repeater), hasName=true
const flags = '82';
const name = Buffer.from('MyNode').toString('hex');
const hex = '1100' + pubKey + ts + sig + flags + name;
const p = decodePacket(hex);
assert.strictEqual(p.payload.flags.hasName, true);
assert.strictEqual(p.payload.name, 'MyNode');
});
test('ADVERT too short', () => {
const hex = '1100' + '00'.repeat(50);
const p = decodePacket(hex);
assert(p.payload.error);
});
console.log('\n=== GRP_TXT payload ===');
test('GRP_TXT basic decode', () => {
// payloadType=5 → (5<<2)|1 = 0x15
const hex = '1500' + 'FF' + 'AABB' + 'CCDDEE';
const p = decodePacket(hex);
assert.strictEqual(p.payload.type, 'GRP_TXT');
assert.strictEqual(p.payload.channelHash, 0xFF);
assert.strictEqual(p.payload.mac, 'aabb');
});
test('GRP_TXT too short', () => {
const hex = '1500' + 'FF' + 'AA';
const p = decodePacket(hex);
assert(p.payload.error);
});
test('GRP_TXT has channelHashHex field', () => {
const hex = '1500' + '1A' + 'AABB' + 'CCDDEE';
const p = decodePacket(hex);
assert.strictEqual(p.payload.channelHashHex, '1A');
});
test('GRP_TXT channelHashHex zero-pads single digit', () => {
const hex = '1500' + '03' + 'AABB' + 'CCDDEE';
const p = decodePacket(hex);
assert.strictEqual(p.payload.channelHashHex, '03');
});
test('GRP_TXT decryptionStatus is no_key when no keys provided', () => {
const hex = '1500' + 'FF' + 'AABB' + 'CCDDEE112233';
const p = decodePacket(hex);
assert.strictEqual(p.payload.decryptionStatus, 'no_key');
});
test('GRP_TXT decryptionStatus is no_key when keys empty', () => {
const hex = '1500' + 'FF' + 'AABB' + 'CCDDEE112233';
const p = decodePacket(hex, {});
assert.strictEqual(p.payload.decryptionStatus, 'no_key');
});
test('GRP_TXT decryptionStatus is decryption_failed with bad keys', () => {
const hex = '1500' + 'FF' + 'AABB' + 'CCDDEE112233';
const p = decodePacket(hex, { '#test': 'deadbeefdeadbeefdeadbeefdeadbeef' });
assert.strictEqual(p.payload.decryptionStatus, 'decryption_failed');
});
test('GRP_TXT decryptionStatus is no_key when encrypted data too short', () => {
// encryptedData < 10 hex chars (5 bytes) — not enough to attempt decryption
const hex = '1500' + 'FF' + 'AABB' + 'CCDD';
const p = decodePacket(hex, { '#test': 'deadbeefdeadbeefdeadbeefdeadbeef' });
assert.strictEqual(p.payload.decryptionStatus, 'no_key');
});
test('GRP_TXT decryptionStatus is decrypted when key matches', () => {
// Mock the ChannelCrypto module to simulate successful decryption
const cryptoPath = require.resolve('@michaelhart/meshcore-decoder/dist/crypto/channel-crypto');
const originalModule = require.cache[cryptoPath];
require.cache[cryptoPath] = {
id: cryptoPath,
exports: {
ChannelCrypto: {
decryptGroupTextMessage: () => ({
success: true,
data: { sender: 'TestUser', message: 'Hello world', timestamp: 1700000000, flags: 0 },
}),
},
},
};
try {
const hex = '1500' + 'FF' + 'AABB' + 'CCDDEE112233';
const p = decodePacket(hex, { '#general': 'aabbccddaabbccddaabbccddaabbccdd' });
assert.strictEqual(p.payload.decryptionStatus, 'decrypted');
assert.strictEqual(p.payload.type, 'CHAN');
assert.strictEqual(p.payload.channelHashHex, 'FF');
assert.strictEqual(p.payload.channel, '#general');
assert.strictEqual(p.payload.sender, 'TestUser');
assert.strictEqual(p.payload.text, 'TestUser: Hello world');
assert.strictEqual(p.payload.sender_timestamp, 1700000000);
assert.strictEqual(p.payload.flags, 0);
assert.strictEqual(p.payload.channelHash, 0xFF);
} finally {
if (originalModule) require.cache[cryptoPath] = originalModule;
else delete require.cache[cryptoPath];
}
});
test('GRP_TXT decrypted without sender formats text correctly', () => {
const cryptoPath = require.resolve('@michaelhart/meshcore-decoder/dist/crypto/channel-crypto');
const originalModule = require.cache[cryptoPath];
require.cache[cryptoPath] = {
id: cryptoPath,
exports: {
ChannelCrypto: {
decryptGroupTextMessage: () => ({
success: true,
data: { sender: null, message: 'Broadcast msg', timestamp: 1700000001, flags: 1 },
}),
},
},
};
try {
const hex = '1500' + '0A' + 'AABB' + 'CCDDEE112233';
const p = decodePacket(hex, { '#alerts': 'deadbeefdeadbeefdeadbeefdeadbeef' });
assert.strictEqual(p.payload.decryptionStatus, 'decrypted');
assert.strictEqual(p.payload.sender, null);
assert.strictEqual(p.payload.text, 'Broadcast msg');
assert.strictEqual(p.payload.channelHashHex, '0A');
} finally {
if (originalModule) require.cache[cryptoPath] = originalModule;
else delete require.cache[cryptoPath];
}
});
test('GRP_TXT decrypted tries multiple keys, first match wins', () => {
const cryptoPath = require.resolve('@michaelhart/meshcore-decoder/dist/crypto/channel-crypto');
const originalModule = require.cache[cryptoPath];
let callCount = 0;
require.cache[cryptoPath] = {
id: cryptoPath,
exports: {
ChannelCrypto: {
decryptGroupTextMessage: (ciphertext, mac, key) => {
callCount++;
if (key === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb') {
return { success: true, data: { sender: 'Bob', message: 'Found it', timestamp: 0, flags: 0 } };
}
return { success: false };
},
},
},
};
try {
const hex = '1500' + 'FF' + 'AABB' + 'CCDDEE112233';
const p = decodePacket(hex, {
'#wrong': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
'#right': 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
});
assert.strictEqual(p.payload.decryptionStatus, 'decrypted');
assert.strictEqual(p.payload.channel, '#right');
assert.strictEqual(p.payload.sender, 'Bob');
assert.strictEqual(callCount, 2);
} finally {
if (originalModule) require.cache[cryptoPath] = originalModule;
else delete require.cache[cryptoPath];
}
});
console.log('\n=== TXT_MSG payload ===');
test('TXT_MSG decode', () => {
// payloadType=2 → (2<<2)|1 = 0x09
const hex = '0900' + '00'.repeat(20);
const p = decodePacket(hex);
assert.strictEqual(p.payload.type, 'TXT_MSG');
assert(p.payload.destHash);
assert(p.payload.srcHash);
assert(p.payload.mac);
});
console.log('\n=== ACK payload ===');
test('ACK decode', () => {
// payloadType=3 → (3<<2)|1 = 0x0D
const hex = '0D00' + '00'.repeat(18);
const p = decodePacket(hex);
assert.strictEqual(p.payload.type, 'ACK');
assert(p.payload.destHash);
assert(p.payload.srcHash);
assert(p.payload.extraHash);
});
test('ACK too short', () => {
const hex = '0D00' + '00'.repeat(3);
const p = decodePacket(hex);
assert(p.payload.error);
});
console.log('\n=== REQ payload ===');
test('REQ decode', () => {
// payloadType=0 → (0<<2)|1 = 0x01
const hex = '0100' + '00'.repeat(20);
const p = decodePacket(hex);
assert.strictEqual(p.payload.type, 'REQ');
});
console.log('\n=== RESPONSE payload ===');
test('RESPONSE decode', () => {
// payloadType=1 → (1<<2)|1 = 0x05
const hex = '0500' + '00'.repeat(20);
const p = decodePacket(hex);
assert.strictEqual(p.payload.type, 'RESPONSE');
});
console.log('\n=== ANON_REQ payload ===');
test('ANON_REQ decode', () => {
// payloadType=7 → (7<<2)|1 = 0x1D
const hex = '1D00' + '00'.repeat(50);
const p = decodePacket(hex);
assert.strictEqual(p.payload.type, 'ANON_REQ');
assert(p.payload.destHash);
assert(p.payload.ephemeralPubKey);
assert(p.payload.mac);
});
test('ANON_REQ too short', () => {
const hex = '1D00' + '00'.repeat(20);
const p = decodePacket(hex);
assert(p.payload.error);
});
console.log('\n=== PATH payload ===');
test('PATH decode', () => {
// payloadType=8 → (8<<2)|1 = 0x21
const hex = '2100' + '00'.repeat(20);
const p = decodePacket(hex);
assert.strictEqual(p.payload.type, 'PATH');
assert(p.payload.destHash);
assert(p.payload.srcHash);
});
test('PATH too short', () => {
const hex = '2100' + '00'.repeat(1);
const p = decodePacket(hex);
assert(p.payload.error);
});
console.log('\n=== TRACE payload ===');
test('TRACE decode', () => {
// payloadType=9 → (9<<2)|1 = 0x25
const hex = '2500' + '00'.repeat(12);
const p = decodePacket(hex);
assert.strictEqual(p.payload.type, 'TRACE');
assert.strictEqual(p.payload.flags, 0);
assert(p.payload.tag !== undefined);
assert(p.payload.destHash);
});
test('TRACE too short', () => {
const hex = '2500' + '00'.repeat(5);
const p = decodePacket(hex);
assert(p.payload.error);
});
console.log('\n=== UNKNOWN payload ===');
test('Unknown payload type', () => {
// payloadType=6 → (6<<2)|1 = 0x19
const hex = '1900' + 'DEADBEEF';
const p = decodePacket(hex);
assert.strictEqual(p.payload.type, 'UNKNOWN');
assert(p.payload.raw);
});
// === Edge cases ===
console.log('\n=== Edge cases ===');
test('Packet too short throws', () => {
assert.throws(() => decodePacket('FF'), /too short/);
});
test('Packet with spaces in hex', () => {
const hex = '11 00 ' + '00'.repeat(101);
const p = decodePacket(hex);
assert.strictEqual(p.header.payloadTypeName, 'ADVERT');
});
test('Transport route too short throws', () => {
assert.throws(() => decodePacket('0000'), /too short for transport/);
});
test('Corrupt packet #183 — path overflow capped to buffer', () => {
const hex = 'BBAD6797EC8751D500BF95A1A776EF580E665BCBF6A0BBE03B5E730707C53489B8C728FD3FB902397197E1263CEC21E52465362243685DBBAD6797EC8751C90A75D9FD8213155D';
const p = decodePacket(hex);
assert.strictEqual(p.header.routeType, 3, 'routeType should be TRANSPORT_DIRECT');
assert.strictEqual(p.header.payloadTypeName, 'UNKNOWN');
// pathByte 0xAD claims 45 hops × 3 bytes = 135, but only 65 bytes available
assert.strictEqual(p.path.hashSize, 3);
assert.strictEqual(p.path.hashCount, 21, 'hashCount capped to fit buffer');
assert.strictEqual(p.path.hops.length, 21);
assert.strictEqual(p.path.truncated, true);
// No empty strings in hops
assert(p.path.hops.every(h => h.length > 0), 'no empty hops');
});
test('path.truncated is false for normal packets', () => {
const hex = '1100' + '00'.repeat(101);
const p = decodePacket(hex);
assert.strictEqual(p.path.truncated, false);
});
test('path overflow with hashSize=2', () => {
// FLOOD + REQ, pathByte=0x45 → hashSize=2, hashCount=5, needs 10 bytes of path
// Only provide 7 bytes after pathByte → fits 3 full 2-byte hops
const hex = '0145' + 'AABBCCDDEEFF77';
const p = decodePacket(hex);
assert.strictEqual(p.path.hashCount, 3);
assert.strictEqual(p.path.truncated, true);
assert.strictEqual(p.path.hops.length, 3);
assert.strictEqual(p.path.hops[0], 'AABB');
assert.strictEqual(p.path.hops[1], 'CCDD');
assert.strictEqual(p.path.hops[2], 'EEFF');
});
// === Real packets from API ===
console.log('\n=== Real packets ===');
test('Real GRP_TXT packet', () => {
const p = decodePacket('150115D96CFF1FC90E7917B91729B76C1B509AE7789BBBD87D5AC3837E6C1487B47B0958AED8C7A6');
assert.strictEqual(p.header.payloadTypeName, 'GRP_TXT');
assert.strictEqual(p.header.routeTypeName, 'FLOOD');
assert.strictEqual(p.path.hashCount, 1);
});
test('Real ADVERT packet FLOOD with 3 hops', () => {
const p = decodePacket('11036CEF52206D763E1EACFD52FBAD4EF926887D0694C42A618AAF480A67C41120D3785950EFE0C1');
assert.strictEqual(p.header.payloadTypeName, 'ADVERT');
assert.strictEqual(p.header.routeTypeName, 'FLOOD');
assert.strictEqual(p.path.hashCount, 3);
assert.strictEqual(p.path.hashSize, 1);
// Payload is too short for full ADVERT but decoder handles it
assert.strictEqual(p.payload.type, 'ADVERT');
});
test('Real DIRECT TXT_MSG packet', () => {
// 0x0A = DIRECT(2) + TXT_MSG(2)
const p = decodePacket('0A403220AD034C0394C2C449810E3D86399C53AEE7FE355BA67002FFC3627B1175A257A181AE');
assert.strictEqual(p.header.payloadTypeName, 'TXT_MSG');
assert.strictEqual(p.header.routeTypeName, 'DIRECT');
});
// === validateAdvert ===
console.log('\n=== validateAdvert ===');
test('valid advert', () => {
const a = { pubKey: 'AB'.repeat(16), flags: { repeater: true, room: false, sensor: false } };
assert.deepStrictEqual(validateAdvert(a), { valid: true });
});
test('null advert', () => {
assert.strictEqual(validateAdvert(null).valid, false);
});
test('advert with error', () => {
assert.strictEqual(validateAdvert({ error: 'bad' }).valid, false);
});
test('pubkey too short', () => {
assert.strictEqual(validateAdvert({ pubKey: 'AABB' }).valid, false);
});
test('pubkey all zeros', () => {
assert.strictEqual(validateAdvert({ pubKey: '0'.repeat(64) }).valid, false);
});
test('invalid lat', () => {
assert.strictEqual(validateAdvert({ pubKey: 'AB'.repeat(16), lat: 200 }).valid, false);
});
test('invalid lon', () => {
assert.strictEqual(validateAdvert({ pubKey: 'AB'.repeat(16), lon: -200 }).valid, false);
});
test('name with control chars', () => {
assert.strictEqual(validateAdvert({ pubKey: 'AB'.repeat(16), name: 'test\x00bad' }).valid, false);
});
test('name too long', () => {
assert.strictEqual(validateAdvert({ pubKey: 'AB'.repeat(16), name: 'A'.repeat(65) }).valid, false);
});
test('valid name', () => {
assert.strictEqual(validateAdvert({ pubKey: 'AB'.repeat(16), name: 'My Node' }).valid, true);
});
test('valid lat/lon', () => {
const r = validateAdvert({ pubKey: 'AB'.repeat(16), lat: 37.3, lon: -121.9 });
assert.strictEqual(r.valid, true);
});
test('NaN lat invalid', () => {
assert.strictEqual(validateAdvert({ pubKey: 'AB'.repeat(16), lat: NaN }).valid, false);
});
// --- GRP_TXT garbage detection (fixes #197) ---
test('GRP_TXT decrypted garbage text marked as decryption_failed', () => {
const cryptoPath = require.resolve('@michaelhart/meshcore-decoder/dist/crypto/channel-crypto');
const originalModule = require.cache[cryptoPath];
require.cache[cryptoPath] = {
id: cryptoPath,
exports: {
ChannelCrypto: {
decryptGroupTextMessage: () => ({
success: true,
data: { sender: 'Node', message: '\x01\x02\x03\x80\x81', timestamp: 1700000000, flags: 0 },
}),
},
},
};
try {
const hex = '1500' + 'FF' + 'AABB' + 'CCDDEE112233';
const p = decodePacket(hex, { '#general': 'aabbccddaabbccddaabbccddaabbccdd' });
assert.strictEqual(p.payload.decryptionStatus, 'decryption_failed');
assert.strictEqual(p.payload.text, null);
assert.strictEqual(p.payload.channelHashHex, 'FF');
assert.strictEqual(p.payload.channel, '#general');
} finally {
if (originalModule) require.cache[cryptoPath] = originalModule;
else delete require.cache[cryptoPath];
}
});
test('GRP_TXT valid text still marked as decrypted', () => {
const cryptoPath = require.resolve('@michaelhart/meshcore-decoder/dist/crypto/channel-crypto');
const originalModule = require.cache[cryptoPath];
require.cache[cryptoPath] = {
id: cryptoPath,
exports: {
ChannelCrypto: {
decryptGroupTextMessage: () => ({
success: true,
data: { sender: 'Alice', message: 'Hello\nworld', timestamp: 1700000000, flags: 0 },
}),
},
},
};
try {
const hex = '1500' + 'FF' + 'AABB' + 'CCDDEE112233';
const p = decodePacket(hex, { '#general': 'aabbccddaabbccddaabbccddaabbccdd' });
assert.strictEqual(p.payload.decryptionStatus, 'decrypted');
assert.strictEqual(p.payload.text, 'Alice: Hello\nworld');
} finally {
if (originalModule) require.cache[cryptoPath] = originalModule;
else delete require.cache[cryptoPath];
}
});
// === Summary ===
console.log(`\n${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);