Files
flynn/src/gateway/discovery.test.ts
T
2026-02-16 01:48:59 -08:00

103 lines
3.1 KiB
TypeScript

import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest';
import { spawn } from 'child_process';
import { EventEmitter } from 'events';
import type { ChildProcess } from 'child_process';
vi.mock('child_process', () => ({
spawn: vi.fn(),
}));
class MockChildProcess extends EventEmitter {
exitCode: number | null = null;
killed = false;
unref = vi.fn();
kill = vi.fn((signal?: NodeJS.Signals) => {
this.killed = true;
this.emit('exit', signal === 'SIGKILL' ? 137 : 0, signal ?? null);
return true;
});
}
const mockSpawn = vi.mocked(spawn);
describe('gateway discovery', () => {
let startGatewayDiscovery: typeof import('./discovery.js').startGatewayDiscovery;
beforeAll(async () => {
const mod = await import('./discovery.js');
startGatewayDiscovery = mod.startGatewayDiscovery;
});
beforeEach(() => {
vi.clearAllMocks();
});
it('starts avahi publisher with txt records', async () => {
const child = new MockChildProcess();
mockSpawn.mockReturnValueOnce(child as unknown as ChildProcess);
setTimeout(() => child.emit('spawn'), 0);
const handle = await startGatewayDiscovery({
serviceName: 'flynn-gateway',
serviceType: '_flynn._tcp',
port: 18800,
txtRecord: { instance: 'pid-123', version: '0.1.0' },
});
expect(mockSpawn).toHaveBeenCalledWith('avahi-publish-service', [
'flynn-gateway',
'_flynn._tcp',
'18800',
'instance=pid-123',
'version=0.1.0',
], { stdio: 'ignore' });
expect(child.unref).toHaveBeenCalledOnce();
await handle.stop();
expect(child.kill).toHaveBeenCalledWith('SIGTERM');
});
it('falls back to dns-sd when avahi is unavailable', async () => {
const avahiChild = new MockChildProcess();
const dnsChild = new MockChildProcess();
mockSpawn.mockReturnValueOnce(avahiChild as unknown as ChildProcess);
mockSpawn.mockReturnValueOnce(dnsChild as unknown as ChildProcess);
setTimeout(() => avahiChild.emit('error', new Error('ENOENT')), 0);
setTimeout(() => dnsChild.emit('spawn'), 0);
const handle = await startGatewayDiscovery({
serviceName: 'flynn-gateway',
serviceType: '_flynn._tcp',
port: 18800,
});
expect(mockSpawn).toHaveBeenNthCalledWith(2, 'dns-sd', [
'-R',
'flynn-gateway',
'_flynn._tcp',
'local',
'18800',
], { stdio: 'ignore' });
await handle.stop();
expect(dnsChild.kill).toHaveBeenCalledWith('SIGTERM');
});
it('throws when no supported advertiser command is available', async () => {
const avahiChild = new MockChildProcess();
const dnsChild = new MockChildProcess();
mockSpawn.mockReturnValueOnce(avahiChild as unknown as ChildProcess);
mockSpawn.mockReturnValueOnce(dnsChild as unknown as ChildProcess);
setTimeout(() => avahiChild.emit('error', new Error('ENOENT')), 0);
setTimeout(() => dnsChild.emit('error', new Error('ENOENT')), 0);
await expect(startGatewayDiscovery({
serviceName: 'flynn-gateway',
serviceType: '_flynn._tcp',
port: 18800,
})).rejects.toThrow(/Failed to start mDNS advertiser/);
});
});