Files
tcp-ingestion/test/codec8.test.ts
T
julian 381287bacc Implement Phase 1 tasks 1.5-1.7 + 1.9 (Codec 8/8E/16 parsers + fixture suite)
- Codec 8 parser (1-byte IO IDs, no NX/Generation Type)
- Codec 8 Extended parser (2-byte IO IDs + variable-length NX section)
- Codec 16 parser (mixed widths + Generation Type, supports IO IDs > 255)
- Shared GPS element / timestamp helpers in gps-element.ts
- Fixture loader with bigint/Buffer sentinel encoding and auto-discovery
- 12 fixture pairs across codec8/8E/16 (canonical doc + synthetic edge cases)
- Cross-checked Codec 8 against Traccar's TeltonikaProtocolDecoder (no discrepancies)

26 new tests. Total 62 passing across 10 test files.
typecheck/lint/test/build all clean.
2026-04-30 16:24:17 +02:00

121 lines
4.3 KiB
TypeScript

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);
});
});