import net from 'net'; import request from 'supertest'; import { describe, expect, it, vi } from 'vitest'; import { WebSocket, WebSocketServer } from 'ws'; import { createServer } from './server.js'; describe('Server', () => { it('server starts and stops', async () => { const server = createServer(); await new Promise(resolve => server.listen(resolve)); await new Promise(resolve => server.close(() => resolve())); }); it('should redirect root requests to landing page', async () => { const server = createServer(); const res = await request(server).get('/'); expect(res.headers.location).toBe('https://pipenet.dev/'); }); it('should support custom base domains', async () => { const server = createServer({ domains: ['domain.example.com'], }); const res = await request(server).get('/'); expect(res.headers.location).toBe('https://pipenet.dev/'); }); it('reject long domain name requests', async () => { const server = createServer(); const res = await request(server).get('/thisdomainisoutsidethesizeofwhatweallowwhichissixtythreecharacters'); expect(res.body.message).toBe('Invalid subdomain. Subdomains must be lowercase and between 4 and 63 alphanumeric characters.'); }); it('should upgrade websocket requests', async () => { const hostname = 'websocket-test'; const server = createServer({ domains: ['example.com'], }); await new Promise(resolve => server.listen(resolve)); const res = await request(server).get('/websocket-test'); const localTunnelPort = res.body.port; const wss = await new Promise((resolve) => { const wsServer = new WebSocketServer({ port: 0 }, () => { resolve(wsServer); }); }); const websocketServerPort = wss.address().port; const ltSocket = net.createConnection({ port: localTunnelPort }); const wsSocket = net.createConnection({ port: websocketServerPort }); // Wait for both sockets to connect await Promise.all([ new Promise(resolve => ltSocket.once('connect', resolve)), new Promise(resolve => wsSocket.once('connect', resolve)), ]); ltSocket.pipe(wsSocket).pipe(ltSocket); wss.once('connection', (ws) => { ws.once('message', (message) => { ws.send(message); }); }); const ws = new WebSocket('http://localhost:' + server.address().port, { headers: { host: hostname + '.example.com', } }); ws.on('open', () => { ws.send('something'); }); await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('WebSocket message timeout')), 10000); ws.once('message', (msg) => { clearTimeout(timeout); expect(msg.toString()).toBe('something'); resolve(); }); ws.once('error', (err) => { clearTimeout(timeout); reject(err); }); }); ws.close(); ltSocket.destroy(); wsSocket.destroy(); wss.close(); await new Promise(resolve => server.close(() => resolve())); }); it('should support the /api/tunnels/:id/status endpoint', async () => { const server = createServer(); await new Promise(resolve => server.listen(resolve)); // no such tunnel yet const res = await request(server).get('/api/tunnels/foobar-test/status'); expect(res.statusCode).toBe(404); // request a new client called foobar-test await request(server).get('/foobar-test'); { const res = await request(server).get('/api/tunnels/foobar-test/status'); expect(res.statusCode).toBe(200); expect(res.body).toEqual({ connectedSockets: 0, }); } await new Promise(resolve => server.close(() => resolve())); }); it('should include CORS headers in responses', async () => { const server = createServer(); const res = await request(server).get('/api/status'); expect(res.headers['access-control-allow-origin']).toBe('*'); expect(res.headers['access-control-allow-methods']).toBe('GET, POST, PUT, DELETE, OPTIONS'); expect(res.headers['access-control-allow-headers']).toBe('Content-Type, Authorization'); }); it('should handle OPTIONS preflight requests', async () => { const server = createServer(); const res = await request(server).options('/api/status'); expect(res.statusCode).toBe(204); expect(res.headers['access-control-allow-origin']).toBe('*'); }); describe('hooks', () => { it('should call onTunnelCreated when a tunnel is created', async () => { const onTunnelCreated = vi.fn(); const server = createServer({ onTunnelCreated }); await new Promise(resolve => server.listen(resolve)); await request(server).get('/test-tunnel'); expect(onTunnelCreated).toHaveBeenCalledTimes(1); expect(onTunnelCreated).toHaveBeenCalledWith(expect.objectContaining({ domain: expect.any(String), id: 'test-tunnel', url: expect.stringContaining('test-tunnel'), })); await new Promise(resolve => server.close(() => resolve())); }); it('should include domain in tunnel hooks', async () => { const onTunnelCreated = vi.fn(); const server = createServer({ domains: ['tunnel.example.com'], onTunnelCreated, }); await new Promise(resolve => server.listen(resolve)); await request(server) .get('/domain-test') .set('Host', 'tunnel.example.com'); expect(onTunnelCreated).toHaveBeenCalledTimes(1); expect(onTunnelCreated).toHaveBeenCalledWith({ domain: 'tunnel.example.com', id: 'domain-test', url: 'http://domain-test.tunnel.example.com', }); await new Promise(resolve => server.close(() => resolve())); }); it('should call onTunnelClosed when a tunnel is closed', async () => { const onTunnelCreated = vi.fn(); const onTunnelClosed = vi.fn(); const server = createServer({ onTunnelClosed, onTunnelCreated }); await new Promise(resolve => server.listen(resolve)); const res = await request(server).get('/close-test'); const localTunnelPort = res.body.port; // Connect a socket to activate the tunnel const socket = net.createConnection({ port: localTunnelPort }); await new Promise(resolve => socket.once('connect', resolve)); expect(onTunnelCreated).toHaveBeenCalledTimes(1); expect(onTunnelClosed).not.toHaveBeenCalled(); // Close the socket to trigger tunnel close socket.destroy(); // Wait for the close event to propagate (Client has a 1s grace timeout after offline) await new Promise(resolve => setTimeout(resolve, 1200)); expect(onTunnelClosed).toHaveBeenCalledTimes(1); expect(onTunnelClosed).toHaveBeenCalledWith(expect.objectContaining({ domain: expect.any(String), id: 'close-test', url: expect.stringContaining('close-test'), })); await new Promise(resolve => server.close(() => resolve())); }, 5000); it('should call onRequest when a request is proxied', async () => { const onRequest = vi.fn(); const server = createServer({ domains: ['example.com'], onRequest, }); await new Promise(resolve => server.listen(resolve)); // Create a tunnel const res = await request(server).get('/request-test'); const localTunnelPort = res.body.port; // Create a simple echo server const echoServer = net.createServer((socket) => { socket.on('data', () => { const response = 'HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK'; socket.write(response); }); }); await new Promise(resolve => echoServer.listen(0, resolve)); const echoPort = echoServer.address().port; // Connect tunnel socket to echo server const ltSocket = net.createConnection({ port: localTunnelPort }); const echoSocket = net.createConnection({ port: echoPort }); await Promise.all([ new Promise(resolve => ltSocket.once('connect', resolve)), new Promise(resolve => echoSocket.once('connect', resolve)), ]); ltSocket.pipe(echoSocket).pipe(ltSocket); // Make a request through the tunnel await request(server) .get('/some/path') .set('Host', 'request-test.example.com'); expect(onRequest).toHaveBeenCalledTimes(1); expect(onRequest).toHaveBeenCalledWith({ headers: expect.objectContaining({ host: 'request-test.example.com', }), method: 'GET', path: '/some/path', remoteAddress: expect.any(String), tunnelId: 'request-test', }); ltSocket.destroy(); echoSocket.destroy(); echoServer.close(); await new Promise(resolve => server.close(() => resolve())); }); }); }); //# sourceMappingURL=server.spec.js.map