import { describe, it, expect, vi } from 'vitest'; import type { Position } from '../src/core/types.js'; import type { CodecHandlerContext } from '../src/adapters/teltonika/codec/registry.js'; import { codec8Handler } from '../src/adapters/teltonika/codec/data/codec8.js'; import { loadFixturesFromDir, compareToExpected } from './fixtures/_loader.js'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // --------------------------------------------------------------------------- // Test context factory // --------------------------------------------------------------------------- function makeTestCtx(positions: Position[]): CodecHandlerContext { return { imei: 'FIXTURE', publish: async (pos: Position) => { positions.push(pos); }, logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), child: vi.fn().mockReturnThis(), } as unknown as CodecHandlerContext['logger'], }; } // --------------------------------------------------------------------------- // Auto-discovered fixture tests // --------------------------------------------------------------------------- const fixtureDir = path.join(__dirname, 'fixtures/teltonika/codec8'); const fixtures = loadFixturesFromDir(fixtureDir); describe('Codec 8 parser — fixture tests', () => { for (const fixture of fixtures) { it(`parses ${fixture.name}`, async () => { const positions: Position[] = []; const ctx = makeTestCtx(positions); const result = await codec8Handler.handle(fixture.body, ctx); expect(result.recordCount).toBe(fixture.expected.ack_record_count); const mismatch = compareToExpected(positions, fixture.expected.positions); expect(mismatch, mismatch ?? '').toBeNull(); }); } }); // --------------------------------------------------------------------------- // Unit tests — edge cases not covered by fixtures // --------------------------------------------------------------------------- describe('Codec 8 parser — unit tests', () => { it('codec_id is 0x08', () => { expect(codec8Handler.codec_id).toBe(0x08); }); it('clamps out-of-range priority (> 2) to 2', async () => { // Priority byte = 0xFF (255), should become 2 (Panic) // Build minimal body: [08][01][TS 8B][Priority=0xFF][GPS 15B zeros][EventId=0][N=0][N1=0][N2=0][N4=0][N8=0][N2=1] const body = Buffer.alloc(33, 0); body[0] = 0x08; // CodecID body[1] = 0x01; // N1 = 1 // Timestamp bytes 2-9 (valid, reuse example 1) Buffer.from('0000016B40D8EA30', 'hex').copy(body, 2); body[10] = 0xff; // Priority = 255 (out of range) // GPS zeros already (bytes 11-25) // IO: all zeros (bytes 26-31) body[32] = 0x01; // trailing N2 const positions: Position[] = []; const ctx = makeTestCtx(positions); await codec8Handler.handle(body, ctx); expect(positions).toHaveLength(1); expect(positions[0]?.priority).toBe(2); }); it('throws on cursor invariant violation (extra trailing bytes)', async () => { // Build a valid single-record body then append extra bytes to trigger the // cursor invariant (cursor ends before N2 position). // Body: [CodecID=08][N1=1][record][N2=1] with extra padding at end. // We pad the body so N2 is not at body.length-1. const validBody = Buffer.from( '08010000016B40D8EA30010000000000000000000000000000000000000000000001', 'hex', ); // Append an extra byte to shift the expected N2 position const tamperedBody = Buffer.concat([validBody, Buffer.from([0x00])]); // Now body.length-1 = 34, but the parser ends at 32 (end of record), // triggering the cursor invariant. const positions: Position[] = []; const ctx = makeTestCtx(positions); await expect(codec8Handler.handle(tamperedBody, ctx)).rejects.toThrow( /cursor invariant violated/, ); }); it('preserves speed=0 verbatim (does not interpret as GPS-invalid)', async () => { const body = Buffer.alloc(33, 0); body[0] = 0x08; body[1] = 0x01; Buffer.from('0000016B40D8EA30', 'hex').copy(body, 2); body[10] = 0x01; // GPS all zeros, including speed = 0x0000 body[32] = 0x01; const positions: Position[] = []; await codec8Handler.handle(body, makeTestCtx(positions)); expect(positions[0]?.speed).toBe(0); }); });