import { tap, expect } from '@git.zone/tstest/tapbundle'; import { SmartProxy } from '../ts/index.js'; import * as plugins from '../ts/plugins.js'; /** * Test that verifies ACME challenge routes are properly created */ tap.test('should create ACME challenge route', async (tools) => { tools.timeout(5000); // Create a challenge route manually to test its structure const challengeRoute = { name: 'acme-challenge', priority: 1000, match: { ports: 18080, path: '/.well-known/acme-challenge/*' }, action: { type: 'socket-handler' as const, socketHandler: (socket: any, context: any) => { socket.once('data', (data: Buffer) => { const request = data.toString(); const lines = request.split('\r\n'); const [method, path] = lines[0].split(' '); const token = path?.split('/').pop() || ''; const response = [ 'HTTP/1.1 200 OK', 'Content-Type: text/plain', `Content-Length: ${token.length}`, 'Connection: close', '', token ].join('\r\n'); socket.write(response); socket.end(); }); } } }; // Test that the challenge route has the correct structure expect(challengeRoute).toBeDefined(); expect(challengeRoute.match.path).toEqual('/.well-known/acme-challenge/*'); expect(challengeRoute.match.ports).toEqual(18080); expect(challengeRoute.action.type).toEqual('socket-handler'); expect(challengeRoute.priority).toEqual(1000); // Create a proxy with the challenge route const settings = { routes: [ { name: 'secure-route', match: { ports: [18443], domains: 'test.local' }, action: { type: 'forward' as const, target: { host: 'localhost', port: 8080 } } }, challengeRoute ] }; const proxy = new SmartProxy(settings); // Mock NFTables manager (proxy as any).nftablesManager = { ensureNFTablesSetup: async () => {}, stop: async () => {} }; // Mock certificate manager to prevent real ACME initialization (proxy as any).createCertificateManager = async function() { return { setUpdateRoutesCallback: () => {}, setHttpProxy: () => {}, setGlobalAcmeDefaults: () => {}, setAcmeStateManager: () => {}, initialize: async () => {}, provisionAllCertificates: async () => {}, stop: async () => {}, getAcmeOptions: () => ({}), getState: () => ({ challengeRouteActive: false }) }; }; await proxy.start(); // Verify the challenge route is in the proxy's routes const proxyRoutes = proxy.routeManager.getAllRoutes(); const foundChallengeRoute = proxyRoutes.find((r: any) => r.name === 'acme-challenge'); expect(foundChallengeRoute).toBeDefined(); expect(foundChallengeRoute?.match.path).toEqual('/.well-known/acme-challenge/*'); await proxy.stop(); }); tap.test('should handle HTTP request parsing correctly', async (tools) => { tools.timeout(5000); let handlerCalled = false; let receivedContext: any; let parsedRequest: any = {}; const settings = { routes: [ { name: 'test-static', match: { ports: [18090], path: '/test/*' }, action: { type: 'socket-handler' as const, socketHandler: (socket, context) => { handlerCalled = true; receivedContext = context; // Parse HTTP request from socket socket.once('data', (data) => { const request = data.toString(); const lines = request.split('\r\n'); const [method, path, protocol] = lines[0].split(' '); // Parse headers const headers: any = {}; for (let i = 1; i < lines.length; i++) { if (lines[i] === '') break; const [key, value] = lines[i].split(': '); if (key && value) { headers[key.toLowerCase()] = value; } } // Store parsed request data parsedRequest = { method, path, headers }; // Send HTTP response const response = [ 'HTTP/1.1 200 OK', 'Content-Type: text/plain', 'Content-Length: 2', 'Connection: close', '', 'OK' ].join('\r\n'); socket.write(response); socket.end(); }); } } } ] }; const proxy = new SmartProxy(settings); // Mock NFTables manager (proxy as any).nftablesManager = { ensureNFTablesSetup: async () => {}, stop: async () => {} }; await proxy.start(); // Create a simple HTTP request const client = new plugins.net.Socket(); await new Promise((resolve, reject) => { client.connect(18090, 'localhost', () => { // Send HTTP request const request = [ 'GET /test/example HTTP/1.1', 'Host: localhost:18090', 'User-Agent: test-client', '', '' ].join('\r\n'); client.write(request); // Wait for response client.on('data', (data) => { const response = data.toString(); expect(response).toContain('HTTP/1.1 200'); expect(response).toContain('OK'); client.end(); resolve(); }); }); client.on('error', reject); }); // Verify handler was called expect(handlerCalled).toBeTrue(); expect(receivedContext).toBeDefined(); // The context passed to socket handlers is IRouteContext, not HTTP request data expect(receivedContext.port).toEqual(18090); expect(receivedContext.routeName).toEqual('test-static'); // Verify the parsed HTTP request data expect(parsedRequest.path).toEqual('/test/example'); expect(parsedRequest.method).toEqual('GET'); expect(parsedRequest.headers.host).toEqual('localhost:18090'); await proxy.stop(); }); tap.start();