OverviewFilesCommitsIssuesDocsReleasesPeers

host-services / test / parse.test.js

118 lines · javascript ref:
// Parsers against output captured on stortron (2026-10-19).
import test from 'brittle'
import fs from 'bare-fs'
import path from 'bare-path'
import {
  parseProcNetTcp, bindScope, parseSystemctlShow, isVendorUnit, parsePghStatus,
  parseHttpHead, extractTitle, parseArp, parseAvahiBrowse, parseIpAddr, sameOriginPath, pageTitle
} from '../lib/parse.js'

const FIX = path.join(path.dirname(new URL(import.meta.url).pathname), 'fixtures')
const fix = (name) => fs.readFileSync(path.join(FIX, name)).toString()

test('proc/net/tcp: LISTEN rows only, addresses and owners decoded', (t) => {
  const rows = parseProcNetTcp(fix('proc-net-tcp.txt'), 4)
  const jellyfin = rows.find((r) => r.port === 8096)
  t.is(jellyfin.addr, '0.0.0.0')
  t.is(jellyfin.uid, 122)
  t.is(rows.filter((r) => r.port === 8096).length, 1, 'established 8096 connections are not listeners')
  const host = rows.find((r) => r.port === 4042)
  t.is(host.addr, '127.0.0.1')
  t.is(host.uid, 1000)
})

test('proc/net/tcp6: any and loopback addresses', (t) => {
  const rows = parseProcNetTcp(fix('proc-net-tcp6.txt'), 6)
  t.ok(rows.length > 0)
  const https = rows.find((r) => r.port === 443)
  t.is(https.addr, '::')
  t.is(bindScope(https.addr), 'all')
  t.ok(rows.some((r) => r.addr === '::1' && r.port === 631), 'cups on ::1')
})

test('bindScope', (t) => {
  t.is(bindScope('0.0.0.0'), 'all')
  t.is(bindScope('127.0.0.53'), 'local')
  t.is(bindScope('::1'), 'local')
  t.is(bindScope('192.168.50.215'), 'addr')
})

test('systemctl show: blocks become units; vendor vs hand-installed', (t) => {
  const units = parseSystemctlShow(fix('systemctl-show.txt'))
  const j = units.find((u) => u.Id === 'jellyfin.service')
  t.is(j.ActiveState, 'active')
  t.is(j.User, 'jellyfin')
  t.ok(isVendorUnit(j))
  const user = parseSystemctlShow(fix('systemctl-user-show.txt'))
  const k = user.find((u) => u.Id === 'kodi-repo.service')
  t.is(k.ActiveState, 'active')
  t.absent(isVendorUnit(k), 'a unit in ~/.config/systemd is hand-installed')
})

test('pgh app status table', (t) => {
  const apps = parsePghStatus(fix('pgh-app-status.txt'))
  t.is(apps.length, 18)
  const c = apps.find((a) => a.name === 'cthuloid-web')
  t.is(c.running, true)
  t.is(c.pid, 1945148)
  t.is(c.autostart, true)
  t.is(c.version, '0.2.1')
  const w = apps.find((a) => a.name === 'favela-web')
  t.is(w.running, false)
  t.is(w.autostart, false)
  t.is(apps.find((a) => a.name === 'favela-gateway').lastExitCode, 75)
  t.is(apps.find((a) => a.name === 'hyper-tps').lastExitCode, null, 'never started: no exit code')
})

test('HTTP head: status, redirect, title; non-HTTP is null', (t) => {
  const h = parseHttpHead('HTTP/1.1 302 Found\r\nLocation: /login\r\nServer: Kestrel\r\n\r\n')
  t.is(h.code, 302)
  t.is(h.location, '/login')
  t.is(h.server, 'Kestrel')
  const p = parseHttpHead('HTTP/1.0 200 OK\r\n\r\n<html><head><title>\n repos &middot; pear-git-v3 </title>')
  t.is(p.title, 'repos · pear-git-v3')
  t.is(parseHttpHead('SSH-2.0-OpenSSH_9.6p1\r\n'), null)
  t.is(extractTitle('<p>no title</p>'), null)
})

test('redirects followed only on the same port', (t) => {
  t.is(sameOriginPath('/login?returnUrl=%2F', 7878), '/login?returnUrl=%2F')
  t.is(sameOriginPath('http://127.0.0.1:5005/git/repos', 5005), '/git/repos')
  t.is(sameOriginPath('http://stortron.local/x', 80), '/x', 'default port 80')
  t.is(sameOriginPath('web/', 8096), '/web/', 'bare relative path')
  t.is(sameOriginPath('http://127.0.0.1:9999/', 5005), null, 'other port')
  t.is(sameOriginPath('https://127.0.0.1:5005/', 5005), null, 'https')
  t.is(sameOriginPath('//evil.example/', 80), null, 'protocol-relative')
  t.is(sameOriginPath('mailto:x@y', 80), null)
  t.is(sameOriginPath(null, 80), null)
})

test('page title: error pages are not a name', (t) => {
  t.is(pageTitle({ code: 200, title: 'Sonarr' }), 'Sonarr')
  t.is(pageTitle({ code: 404, title: '404 Not Found' }), null)
  t.is(pageTitle({ code: 401, title: 'Unauthorized' }), null)
  t.is(pageTitle({ code: 200, title: 'Directory listing for /' }), null)
  t.is(pageTitle({ code: 200, title: null }), null)
  t.is(pageTitle(null), null)
})

test('arp: complete entries only', (t) => {
  const arp = parseArp(fix('proc-net-arp.txt'))
  t.ok(arp.length > 5)
  t.ok(arp.every((a) => /^[0-9a-f:]{17}$/.test(a.mac) && a.mac !== '00:00:00:00:00:00'))
  t.ok(arp.some((a) => a.ip === '192.168.50.228'))
  t.is(parseArp('IP address HW type Flags HW address Mask Device\n192.168.50.9 0x1 0x0 00:00:00:00:00:00 * enp2s0\n').length, 0)
})

test('avahi browse: resolved IPv4 services', (t) => {
  const svcs = parseAvahiBrowse(fix('avahi-browse.txt'))
  t.ok(svcs.length > 0)
  t.ok(svcs.every((s) => /^\d+\.\d+\.\d+\.\d+$/.test(s.ip) && Number.isInteger(s.port)))
  t.ok(svcs.some((s) => s.type === '_spotify-connect._tcp' && s.ip === '192.168.50.111'))
})

test('ip addr: LAN and tunnel addresses', (t) => {
  const a = parseIpAddr(fix('ip-addr.txt'))
  t.alike(a.map((x) => [x.dev, x.ip, x.prefix]), [['enp2s0', '192.168.50.215', 24], ['tunnel0', '10.22.0.2', 16]])
})