import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest'; import { execFile } from 'child_process'; // Mock child_process before importing module vi.mock('child_process', () => ({ execFile: vi.fn(), })); const mockExecFile = vi.mocked(execFile); describe('tailscale', () => { // Import after mocking let isTailscaleAvailable: typeof import('./tailscale.js').isTailscaleAvailable; let startTailscaleServe: typeof import('./tailscale.js').startTailscaleServe; let stopTailscaleServe: typeof import('./tailscale.js').stopTailscaleServe; beforeAll(async () => { const mod = await import('./tailscale.js'); isTailscaleAvailable = mod.isTailscaleAvailable; startTailscaleServe = mod.startTailscaleServe; stopTailscaleServe = mod.stopTailscaleServe; }); beforeEach(() => { vi.clearAllMocks(); }); describe('isTailscaleAvailable', () => { it('returns available when tailscale CLI works', async () => { mockExecFile .mockImplementationOnce((_cmd, _args, _opts, callback: any) => { callback(null, '1.62.0', ''); return {} as any; }) .mockImplementationOnce((_cmd, _args, _opts, callback: any) => { callback(null, '{}', ''); return {} as any; }); const result = await isTailscaleAvailable(); expect(result.available).toBe(true); expect(result.version).toBe('1.62.0'); }); it('returns unavailable when tailscale CLI fails', async () => { mockExecFile.mockImplementationOnce((_cmd, _args, _opts, callback: any) => { callback(new Error('command not found'), '', 'command not found'); return {} as any; }); const result = await isTailscaleAvailable(); expect(result.available).toBe(false); expect(result.error).toBeDefined(); }); }); describe('startTailscaleServe', () => { it('calls tailscale serve with correct args', async () => { mockExecFile // serve command .mockImplementationOnce((_cmd, _args, _opts, callback: any) => { callback(null, '', ''); return {} as any; }) // status for hostname .mockImplementationOnce((_cmd, _args, _opts, callback: any) => { callback(null, JSON.stringify({ Self: { DNSName: 'myhost.tailnet.ts.net.' } }), ''); return {} as any; }); const url = await startTailscaleServe({ localPort: 18800 }); expect(url).toBe('https://myhost.tailnet.ts.net'); const serveCall = mockExecFile.mock.calls[0]; expect(serveCall[0]).toBe('tailscale'); expect(serveCall[1]).toEqual(['serve', '--bg', '--https=443', 'http://127.0.0.1:18800']); }); it('uses custom serve port', async () => { mockExecFile .mockImplementationOnce((_cmd, _args, _opts, callback: any) => { callback(null, '', ''); return {} as any; }) .mockImplementationOnce((_cmd, _args, _opts, callback: any) => { callback(null, JSON.stringify({ Self: { DNSName: 'myhost.tailnet.ts.net.' } }), ''); return {} as any; }); const url = await startTailscaleServe({ localPort: 18800, servePort: 8443 }); expect(url).toBe('https://myhost.tailnet.ts.net:8443'); const serveCall = mockExecFile.mock.calls[0]; expect(serveCall[1]).toEqual(['serve', '--bg', '--https=8443', 'http://127.0.0.1:18800']); }); }); describe('stopTailscaleServe', () => { it('calls tailscale serve off', async () => { mockExecFile.mockImplementationOnce((_cmd, _args, _opts, callback: any) => { callback(null, '', ''); return {} as any; }); await stopTailscaleServe({ localPort: 18800 }); expect(mockExecFile).toHaveBeenCalledOnce(); const call = mockExecFile.mock.calls[0]; expect(call[0]).toBe('tailscale'); expect(call[1]).toEqual(['serve', '--https=443', 'off']); }); it('does not throw on failure', async () => { mockExecFile.mockImplementationOnce((_cmd, _args, _opts, callback: any) => { callback(new Error('failed'), '', 'failed'); return {} as any; }); // Should not throw await stopTailscaleServe({ localPort: 18800 }); }); }); });