// 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 · 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]])
})