156 lines
5.7 KiB
JavaScript
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)
|
|
})
|
|
})
|
|
})
|