import { describe, it, expect, vi } from 'vitest'; import { EventEmitter } from 'node:events'; import type * as net from 'node:net'; import { readImeiHandshake, HandshakeError } from '../src/adapters/teltonika/handshake.js'; function makeMockSocket(): net.Socket & { push(buf: Buffer): void } { const emitter = new EventEmitter() as net.Socket & { push(buf: Buffer): void; destroyed: boolean; write: ReturnType; destroy: ReturnType; }; emitter.destroyed = false; emitter.write = vi.fn().mockReturnValue(true); emitter.destroy = vi.fn(() => { emitter.destroyed = true; emitter.emit('close', false); }); emitter.push = (buf: Buffer) => emitter.emit('data', buf); return emitter; } /** * Encodes a Teltonika IMEI handshake frame: * [2B length big-endian][IMEI ASCII bytes] */ function encodeImeiHandshake(imei: string): Buffer { const imeiBytes = Buffer.from(imei, 'ascii'); const header = Buffer.alloc(2); header.writeUInt16BE(imeiBytes.length, 0); return Buffer.concat([header, imeiBytes]); } describe('readImeiHandshake', () => { it('parses a valid 15-digit IMEI without writing to the socket', async () => { const socket = makeMockSocket(); const imei = '356307042441013'; const handshakeBytes = encodeImeiHandshake(imei); const resultPromise = readImeiHandshake(socket); socket.push(handshakeBytes); const result = await resultPromise; expect(result).toBe(imei); // Must NOT write anything — accept/reject is the session loop's job expect(socket.write).not.toHaveBeenCalled(); }); it('parses a valid IMEI arriving in two chunks (header + body split)', async () => { const socket = makeMockSocket(); const imei = '123456789012345'; const handshakeBytes = encodeImeiHandshake(imei); const resultPromise = readImeiHandshake(socket); // Split: first push just the 2-byte header, then the IMEI bytes socket.push(handshakeBytes.subarray(0, 2)); socket.push(handshakeBytes.subarray(2)); const result = await resultPromise; expect(result).toBe(imei); }); it('parses the Teltonika doc example IMEI (356307042441013)', async () => { // From the doc: IMEI 356307042441013 encoded as 000F333536333037303432343431303133 const socket = makeMockSocket(); const frame = Buffer.from('000F333536333037303432343431303133', 'hex'); const resultPromise = readImeiHandshake(socket); socket.push(frame); const result = await resultPromise; expect(result).toBe('356307042441013'); }); it('throws HandshakeError for IMEI with non-digit characters', async () => { const socket = makeMockSocket(); const badImei = 'ABCDEFGHIJKLMNO'; // 15 chars, not digits const frame = encodeImeiHandshake(badImei); const resultPromise = readImeiHandshake(socket); socket.push(frame); await expect(resultPromise).rejects.toBeInstanceOf(HandshakeError); expect(socket.write).not.toHaveBeenCalled(); }); it('throws HandshakeError for IMEI that is too short (< 14 digits)', async () => { const socket = makeMockSocket(); const shortImei = '1234567890123'; // 13 digits const frame = encodeImeiHandshake(shortImei); const resultPromise = readImeiHandshake(socket); socket.push(frame); await expect(resultPromise).rejects.toBeInstanceOf(HandshakeError); }); it('throws HandshakeError for zero-length IMEI', async () => { const socket = makeMockSocket(); const frame = Buffer.from([0x00, 0x00]); // length = 0 const resultPromise = readImeiHandshake(socket); socket.push(frame); await expect(resultPromise).rejects.toBeInstanceOf(HandshakeError); expect(socket.write).not.toHaveBeenCalled(); }); it('throws HandshakeError for excessively long IMEI length field (> 32)', async () => { const socket = makeMockSocket(); const frame = Buffer.alloc(2); frame.writeUInt16BE(33, 0); // length = 33 const resultPromise = readImeiHandshake(socket); socket.push(frame); await expect(resultPromise).rejects.toBeInstanceOf(HandshakeError); }); });