Files
kestrelos/test/unit/cotParser.spec.js
Madison Grubb b0e8dd7ad9
Some checks failed
ci/woodpecker/pr/pr Pipeline failed
make kestrel a tak server, so that it can send and receive pois as cots data
2026-02-17 10:42:53 -05:00

156 lines
5.7 KiB
JavaScript

import { describe, it, expect } from 'vitest'
import { parseTakStreamFrame, parseTraditionalXmlFrame, parseCotPayload } from '../../../server/utils/cotParser.js'
function buildTakFrame(payload) {
const buf = Buffer.from(payload, 'utf8')
let n = buf.length
const varint = []
while (true) {
const byte = n & 0x7F
n >>>= 7
if (n === 0) {
varint.push(byte)
break
}
varint.push(byte | 0x80)
}
return Buffer.concat([Buffer.from([0xBF]), Buffer.from(varint), buf])
}
describe('cotParser', () => {
describe('parseTakStreamFrame', () => {
it('parses valid frame', () => {
const payload = '<event uid="x" type="a-f-G"><point lat="1" lon="2"/></event>'
const frame = buildTakFrame(payload)
const result = parseTakStreamFrame(frame)
expect(result).not.toBeNull()
expect(result.payload.toString('utf8')).toBe(payload)
expect(result.bytesConsumed).toBe(frame.length)
})
it('returns null for incomplete buffer', () => {
const frame = buildTakFrame('<e/>')
const partial = frame.subarray(0, 2)
expect(parseTakStreamFrame(partial)).toBeNull()
})
it('returns null for wrong magic', () => {
const payload = '<e/>'
const buf = Buffer.concat([Buffer.from([0x00]), Buffer.from([payload.length]), Buffer.from(payload)])
expect(parseTakStreamFrame(buf)).toBeNull()
})
it('returns null for payload length exceeding max', () => {
const hugeLen = 64 * 1024 + 1
const varint = []
let n = hugeLen
while (true) {
varint.push(n & 0x7F)
n >>>= 7
if (n === 0) break
varint[varint.length - 1] |= 0x80
}
const buf = Buffer.concat([Buffer.from([0xBF]), Buffer.from(varint)])
expect(parseTakStreamFrame(buf)).toBeNull()
})
})
describe('parseTraditionalXmlFrame', () => {
it('parses one XML message delimited by </event>', () => {
const xml = '<event uid="x" type="a-f-G"><point lat="1" lon="2"/></event>'
const buf = Buffer.from(xml, 'utf8')
const result = parseTraditionalXmlFrame(buf)
expect(result).not.toBeNull()
expect(result.payload.toString('utf8')).toBe(xml)
expect(result.bytesConsumed).toBe(buf.length)
})
it('returns null when buffer does not start with <', () => {
expect(parseTraditionalXmlFrame(Buffer.from('x<event></event>'))).toBeNull()
expect(parseTraditionalXmlFrame(Buffer.from([0xBF, 0x00]))).toBeNull()
})
it('returns null when </event> not yet received', () => {
const partial = Buffer.from('<event uid="x"><point lat="1" lon="2"/>', 'utf8')
expect(parseTraditionalXmlFrame(partial)).toBeNull()
})
it('extracted payload parses as auth CoT', () => {
const xml = '<event><detail><auth username="itak" password="mypass"/></detail></event>'
const buf = Buffer.from(xml, 'utf8')
const result = parseTraditionalXmlFrame(buf)
expect(result).not.toBeNull()
const parsed = parseCotPayload(result.payload)
expect(parsed).not.toBeNull()
expect(parsed.type).toBe('auth')
expect(parsed.username).toBe('itak')
expect(parsed.password).toBe('mypass')
})
})
describe('parseCotPayload', () => {
it('parses position CoT XML', () => {
const xml = '<event uid="device-1" type="a-f-G-U-C"><point lat="37.7" lon="-122.4"/><detail><contact callsign="Bravo"/></detail></event>'
const result = parseCotPayload(Buffer.from(xml, 'utf8'))
expect(result).not.toBeNull()
expect(result.type).toBe('cot')
expect(result.id).toBe('device-1')
expect(result.lat).toBe(37.7)
expect(result.lng).toBe(-122.4)
expect(result.label).toBe('Bravo')
})
it('parses auth CoT with detail.auth', () => {
const xml = '<event><detail><auth username="user1" password="secret123"/></detail></event>'
const result = parseCotPayload(Buffer.from(xml, 'utf8'))
expect(result).not.toBeNull()
expect(result.type).toBe('auth')
expect(result.username).toBe('user1')
expect(result.password).toBe('secret123')
})
it('parses auth CoT with __auth', () => {
const xml = '<event><detail><__auth username="u2" password="p2"/></detail></event>'
const result = parseCotPayload(Buffer.from(xml, 'utf8'))
expect(result).not.toBeNull()
expect(result.type).toBe('auth')
expect(result.username).toBe('u2')
expect(result.password).toBe('p2')
})
it('returns null for auth with empty username', () => {
const xml = '<event><detail><auth username=" " password="p"/></detail></event>'
const result = parseCotPayload(Buffer.from(xml, 'utf8'))
expect(result).toBeNull()
})
it('parses position with point.lat and point.lon (no @_ prefix)', () => {
const xml = '<event uid="x" type="a-f-G"><point lat="5" lon="10"/></event>'
const result = parseCotPayload(Buffer.from(xml, 'utf8'))
expect(result).not.toBeNull()
expect(result.lat).toBe(5)
expect(result.lng).toBe(10)
})
it('returns null for non-XML payload', () => {
expect(parseCotPayload(Buffer.from('not xml'))).toBeNull()
})
it('uses uid as label when no contact/callsign', () => {
const xml = '<event uid="device-99" type="a-f-G"><point lat="1" lon="2"/></event>'
const result = parseCotPayload(Buffer.from(xml, 'utf8'))
expect(result).not.toBeNull()
expect(result.type).toBe('cot')
expect(result.label).toBe('device-99')
})
it('uses point inside event when not at root', () => {
const xml = '<event uid="x" type="a-f-G"><point lat="10" lon="20"/></event>'
const result = parseCotPayload(Buffer.from(xml, 'utf8'))
expect(result).not.toBeNull()
expect(result.lat).toBe(10)
expect(result.lng).toBe(20)
})
})
})