fetchtv
Version:
A Node.js CLI tool to manage Fetch TV recordings.
80 lines (69 loc) • 3.52 kB
JavaScript
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import { parseXml, findNode } from '../fetchtv.js'
const fixturesDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures')
const readFixture = (name) =>
readFileSync(path.join(fixturesDir, name), 'utf-8')
const buildEntityRichBrowseResponse = ({ itemCount }) => {
const inner = Array.from({ length: itemCount }, (_, i) =>
`<item id="${1000 + i}" parentID="10" restricted="1">` +
`<dc:title>S1 E${i + 1} - Episode ${i + 1}</dc:title>` +
`<dc:description>Episode ${i + 1} of Season 1</dc:description>` +
`<upnp:class>object.item.videoItem</upnp:class>` +
`<res protocolInfo="http-get:*:video/mpeg-tts:*" size="1073741824" ` +
`duration="1:23:45">http://192.168.1.10/item/${1000 + i}</res>` +
`</item>`
).join('')
return `<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:BrowseResponse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
<Result><DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" ` +
`xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" ` +
`xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">${inner}</DIDL-Lite></Result>
<NumberReturned>${itemCount}</NumberReturned>
<TotalMatches>${itemCount}</TotalMatches>
<UpdateID>1</UpdateID>
</u:BrowseResponse>
</s:Body>
</s:Envelope>`
}
test('parseXml: returns null for empty / non-string input', () => {
assert.equal(parseXml(''), null)
assert.equal(parseXml(null), null)
assert.equal(parseXml(undefined), null)
assert.equal(parseXml(42), null)
})
test('parseXml: parses a simple SOAP Browse response', () => {
const xml = readFixture('browse-root.xml')
const parsed = parseXml(xml)
const result = findNode({ obj: parsed, path: 'Envelope.Body.BrowseResponse.Result' })
assert.equal(typeof result, 'string')
assert.match(result, /DIDL-Lite/)
})
test('parseXml: REGRESSION — Browse response with >1000 entity references still parses', () => {
// fast-xml-parser 4.5.5 introduced a default maxTotalExpansions: 1000 cap.
// 60 items × ~24 entity refs ≈ 1440 expansions in the outer Envelope parse.
// If this test ever fails, the entity cap regression has returned.
const xml = buildEntityRichBrowseResponse({ itemCount: 60 })
const parsed = parseXml(xml)
assert.ok(parsed, 'expected parsed envelope, got null')
const result = findNode({ obj: parsed, path: 'Envelope.Body.BrowseResponse.Result' })
assert.equal(typeof result, 'string', 'Result text must survive entity decode')
assert.match(result, /S1 E60/, 'Result must include all items, not be truncated')
})
test('parseXml: nested Browse Result re-parses to DIDL-Lite with all items', () => {
const xml = buildEntityRichBrowseResponse({ itemCount: 60 })
const outer = parseXml(xml)
const resultText = findNode({ obj: outer, path: 'Envelope.Body.BrowseResponse.Result' })
const inner = parseXml(resultText)
const items = inner['DIDL-Lite'].item
assert.equal(items.length, 60)
})
test('parseXml: tolerates malformed XML without throwing', () => {
const result = parseXml('<not-actually-valid')
assert.equal(result, null)
})