Files
openchamber/packages/web/server/lib/dev-servers/parse.test.js
T

175 lines
6.5 KiB
JavaScript
Raw Normal View History

import { describe, expect, test } from 'bun:test';
import {
isLocallyReachableHost,
parseLsofListeners,
parseProcNetTcpListeners,
parseNetstatListeners,
selectDevServerCandidates,
} from './parse.js';
describe('lsof listener parsing', () => {
test('associates every socket with the process record above it', () => {
const output = [
'p1234', 'cnode', 'n*:5173',
'p5678', 'cpython3', 'n127.0.0.1:8000',
].join('\n');
expect(parseLsofListeners(output)).toEqual([
{ port: 5173, pid: 1234, command: 'node' },
{ port: 8000, pid: 5678, command: 'python3' },
]);
});
test('keeps one entry when a process binds the same port on IPv4 and IPv6', () => {
const output = ['p1234', 'cnode', 'n*:5173', 'n[::1]:5173'].join('\n');
expect(parseLsofListeners(output)).toEqual([{ port: 5173, pid: 1234, command: 'node' }]);
});
test('unwraps bracketed IPv6 addresses', () => {
expect(parseLsofListeners(['p1', 'cnode', 'n[::1]:3000'].join('\n')))
.toEqual([{ port: 3000, pid: 1, command: 'node' }]);
});
test('skips sockets bound only to a LAN address, which localhost cannot reach', () => {
expect(parseLsofListeners(['p1', 'cnode', 'n192.168.1.10:5173'].join('\n'))).toEqual([]);
});
test('skips established connections that slipped past the LISTEN filter', () => {
expect(parseLsofListeners(['p1', 'cnode', 'n127.0.0.1:5173->127.0.0.1:60123'].join('\n')))
.toEqual([]);
});
test('returns sorted results', () => {
const output = ['p1', 'cnode', 'n*:9000', 'n*:3000', 'n*:5173'].join('\n');
expect(parseLsofListeners(output).map((entry) => entry.port)).toEqual([3000, 5173, 9000]);
});
test('tolerates empty and malformed output rather than throwing', () => {
expect(parseLsofListeners('')).toEqual([]);
expect(parseLsofListeners(null)).toEqual([]);
expect(parseLsofListeners('garbage\nn:\nnnotaport')).toEqual([]);
});
test('rejects out-of-range ports', () => {
expect(parseLsofListeners(['p1', 'cnode', 'n*:70000', 'n*:0'].join('\n'))).toEqual([]);
});
});
describe('netstat listener parsing', () => {
const output = [
'Active Connections',
'',
' Proto Local Address Foreign Address State PID',
' TCP 0.0.0.0:5173 0.0.0.0:0 LISTENING 4242',
' TCP 127.0.0.1:8000 0.0.0.0:0 LISTENING 9001',
' TCP 192.168.0.5:9999 0.0.0.0:0 LISTENING 9002',
' TCP 127.0.0.1:5173 127.0.0.1:60123 ESTABLISHED 9003',
].join('\n');
test('takes listening loopback and wildcard sockets with their pid', () => {
expect(parseNetstatListeners(output)).toEqual([
{ port: 5173, pid: 4242, command: '' },
{ port: 8000, pid: 9001, command: '' },
]);
});
test('ignores established connections and LAN-only binds', () => {
const ports = parseNetstatListeners(output).map((entry) => entry.port);
expect(ports).not.toContain(9999);
});
test('tolerates empty output', () => {
expect(parseNetstatListeners('')).toEqual([]);
});
});
describe('host reachability', () => {
test('accepts loopback and wildcard binds', () => {
for (const host of ['127.0.0.1', 'localhost', '[::1]', '*', '0.0.0.0', '[::]']) {
expect(isLocallyReachableHost(host)).toBe(true);
}
});
test('rejects a specific LAN address', () => {
expect(isLocallyReachableHost('192.168.1.4')).toBe(false);
});
});
describe('candidate selection', () => {
const listeners = [
{ port: 5173, pid: 10, command: 'node' },
{ port: 5432, pid: 11, command: 'postgres' },
{ port: 4096, pid: 12, command: 'openchamber' },
{ port: 3000, pid: 13, command: 'node' },
];
test('drops OpenChamber own ports so the app never offers itself', () => {
const ports = selectDevServerCandidates(listeners, { ownPorts: [4096] }).map((entry) => entry.port);
expect(ports).toEqual([5173, 3000]);
});
test('drops sockets owned by our own process', () => {
const ports = selectDevServerCandidates(listeners, { ownPids: [13] }).map((entry) => entry.port);
expect(ports).toEqual([5173, 4096]);
});
test('drops well-known infrastructure ports that are never previewable', () => {
const ports = selectDevServerCandidates(listeners).map((entry) => entry.port);
expect(ports).not.toContain(5432);
});
test('keeps everything else, including unusual ports', () => {
const ports = selectDevServerCandidates([{ port: 12345, pid: 1, command: 'bun' }]).map((entry) => entry.port);
expect(ports).toEqual([12345]);
});
});
describe('proc net tcp parsing', () => {
const header = ' sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode';
test('takes listening sockets on loopback and wildcard binds', () => {
const output = [
header,
' 0: 00000000:1F90 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 12345 1 0000 100 0',
' 1: 0100007F:0BB8 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 12346 1 0000 100 0',
].join('\n');
expect(parseProcNetTcpListeners(output)).toEqual([
{ port: 3000, pid: null, command: '' },
{ port: 8080, pid: null, command: '' },
]);
});
test('ignores sockets that are not listening', () => {
const output = [
header,
' 0: 0100007F:1F90 0100007F:C350 01 00000000:00000000 00:00000000 00000000 1000 0 12345 1 0000 100 0',
].join('\n');
expect(parseProcNetTcpListeners(output)).toEqual([]);
});
test('ignores a bind to a specific LAN address', () => {
const output = [
header,
' 0: 0A00020F:1F90 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 12345 1 0000 100 0',
].join('\n');
expect(parseProcNetTcpListeners(output)).toEqual([]);
});
test('reads the IPv6 table, including ::1 and ::', () => {
const output = [
header,
' 0: 00000000000000000000000001000000:1F90 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 1 1 0 0 0',
' 1: 00000000000000000000000000000000:0BB8 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 1 1 0 0 0',
].join('\n');
expect(parseProcNetTcpListeners(output).map((entry) => entry.port)).toEqual([3000, 8080]);
});
test('tolerates an empty or malformed table', () => {
expect(parseProcNetTcpListeners('')).toEqual([]);
expect(parseProcNetTcpListeners(header)).toEqual([]);
expect(parseProcNetTcpListeners('garbage')).toEqual([]);
});
});