381287bacc
- 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.
121 lines
4.3 KiB
TypeScript
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);
|
|
});
|
|
});
|