feat(gateway): add optional bonjour/mdns discovery
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
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/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user