@elizaos/plugin-ngrok 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +325 -0
- package/dist/__tests__/NgrokTestSuite.d.ts +6 -0
- package/dist/__tests__/NgrokTestSuite.d.ts.map +1 -0
- package/dist/__tests__/NgrokTestSuite.js +92 -0
- package/dist/__tests__/NgrokTestSuite.js.map +1 -0
- package/dist/actions/get-tunnel-status.d.ts +4 -0
- package/dist/actions/get-tunnel-status.d.ts.map +1 -0
- package/dist/actions/get-tunnel-status.js +186 -0
- package/dist/actions/get-tunnel-status.js.map +1 -0
- package/dist/actions/start-tunnel.d.ts +4 -0
- package/dist/actions/start-tunnel.d.ts.map +1 -0
- package/dist/actions/start-tunnel.js +221 -0
- package/dist/actions/start-tunnel.js.map +1 -0
- package/dist/actions/stop-tunnel.d.ts +4 -0
- package/dist/actions/stop-tunnel.d.ts.map +1 -0
- package/dist/actions/stop-tunnel.js +174 -0
- package/dist/actions/stop-tunnel.js.map +1 -0
- package/dist/environment.d.ts +12 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +68 -0
- package/dist/environment.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/services/NgrokService.d.ts +30 -0
- package/dist/services/NgrokService.d.ts.map +1 -0
- package/dist/services/NgrokService.js +333 -0
- package/dist/services/NgrokService.js.map +1 -0
- package/package.json +63 -0
- package/src/__tests__/NgrokTestSuite.ts +110 -0
- package/src/__tests__/debug-mock.test.ts +15 -0
- package/src/__tests__/e2e/real-ngrok.test.ts +543 -0
- package/src/__tests__/integration/webhook-scenarios.test.ts +463 -0
- package/src/__tests__/mocks/NgrokServiceMock.ts +76 -0
- package/src/__tests__/ngrok-integration.test.ts +521 -0
- package/src/__tests__/test-config.ts +83 -0
- package/src/__tests__/test-helpers.ts +43 -0
- package/src/__tests__/test-setup.ts +174 -0
- package/src/__tests__/test-utils.ts +155 -0
- package/src/__tests__/unit/actions.test.ts +402 -0
- package/src/__tests__/unit/environment.test.ts +352 -0
- package/src/actions/get-tunnel-status.ts +218 -0
- package/src/actions/start-tunnel.ts +255 -0
- package/src/actions/stop-tunnel.ts +203 -0
- package/src/environment.ts +75 -0
- package/src/index.ts +33 -0
- package/src/services/NgrokService.ts +401 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, jest } from 'bun:test';
|
|
2
|
+
import * as http from 'node:http';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import type { IAgentRuntime, Memory, State } from '@elizaos/core';
|
|
5
|
+
import { config } from 'dotenv';
|
|
6
|
+
import { getTunnelStatusAction } from '../../actions/get-tunnel-status';
|
|
7
|
+
import { startTunnelAction } from '../../actions/start-tunnel';
|
|
8
|
+
import { stopTunnelAction } from '../../actions/stop-tunnel';
|
|
9
|
+
import { NgrokService } from '../../services/NgrokService';
|
|
10
|
+
import { testConfig, testDelay } from '../test-config';
|
|
11
|
+
import { ngrokSafeDelay } from '../test-helpers';
|
|
12
|
+
|
|
13
|
+
// Load environment variables
|
|
14
|
+
config({ path: path.resolve(process.cwd(), '.env') });
|
|
15
|
+
|
|
16
|
+
type PortExtractionParams = {
|
|
17
|
+
context?: {
|
|
18
|
+
userMessage?: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe('Real ngrok API E2E Tests', () => {
|
|
23
|
+
let runtime: IAgentRuntime;
|
|
24
|
+
let service: NgrokService;
|
|
25
|
+
let testServer: http.Server;
|
|
26
|
+
let testServerPort: number;
|
|
27
|
+
let skipTests = false;
|
|
28
|
+
|
|
29
|
+
beforeAll(async () => {
|
|
30
|
+
// Check if we should skip tests
|
|
31
|
+
const hasAuthToken = Boolean(process.env.NGROK_AUTH_TOKEN);
|
|
32
|
+
const skipEnvVar = process.env.SKIP_NGROK_TESTS === 'true';
|
|
33
|
+
|
|
34
|
+
console.log('E2E test environment check:');
|
|
35
|
+
console.log('- NGROK_AUTH_TOKEN:', hasAuthToken ? 'Set' : 'Not set');
|
|
36
|
+
console.log('- NGROK_DOMAIN:', process.env.NGROK_DOMAIN || 'Not set (will use random URL)');
|
|
37
|
+
console.log('- SKIP_NGROK_TESTS:', skipEnvVar);
|
|
38
|
+
|
|
39
|
+
if (!hasAuthToken || skipEnvVar) {
|
|
40
|
+
skipTests = true;
|
|
41
|
+
console.log('⚠️ Skipping E2E tests');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Add delay before starting the test suite
|
|
46
|
+
await testDelay(testConfig.execution.suitesDelay);
|
|
47
|
+
|
|
48
|
+
// Create a test HTTP server
|
|
49
|
+
testServer = http.createServer((req, res) => {
|
|
50
|
+
const chunks: Buffer[] = [];
|
|
51
|
+
|
|
52
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
53
|
+
req.on('end', () => {
|
|
54
|
+
const body = Buffer.concat(chunks).toString();
|
|
55
|
+
|
|
56
|
+
// Log the request for debugging
|
|
57
|
+
console.log(`Received ${req.method} request to ${req.url}`);
|
|
58
|
+
|
|
59
|
+
// Handle different endpoints
|
|
60
|
+
if (req.url === '/health') {
|
|
61
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
62
|
+
res.end(JSON.stringify({ status: 'healthy', timestamp: new Date().toISOString() }));
|
|
63
|
+
} else if (req.url === '/webhook') {
|
|
64
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
65
|
+
res.end(
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
received: true,
|
|
68
|
+
method: req.method,
|
|
69
|
+
headers: req.headers,
|
|
70
|
+
body: body ? JSON.parse(body) : null,
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
} else if (req.url === '/echo') {
|
|
74
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
75
|
+
res.end(body || JSON.stringify({ echo: 'empty' }));
|
|
76
|
+
} else {
|
|
77
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
78
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Start server on random port
|
|
84
|
+
await new Promise<void>((resolve) => {
|
|
85
|
+
testServer.listen(0, () => {
|
|
86
|
+
const address = testServer.address();
|
|
87
|
+
if (address && typeof address === 'object') {
|
|
88
|
+
testServerPort = address.port;
|
|
89
|
+
}
|
|
90
|
+
console.log(`✅ Test server started on port ${testServerPort}`);
|
|
91
|
+
resolve();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Create runtime with real environment
|
|
96
|
+
runtime = {
|
|
97
|
+
agentId: 'test-agent',
|
|
98
|
+
getSetting: (key: string) => process.env[key],
|
|
99
|
+
getService: (name: string) => (name === 'tunnel' ? service : undefined),
|
|
100
|
+
registerService: (_service: unknown) => {},
|
|
101
|
+
useModel: async (_model: string, params: PortExtractionParams) => {
|
|
102
|
+
// Return the port from the message content
|
|
103
|
+
const portMatch = params.context?.userMessage?.match(/port (\d+)/);
|
|
104
|
+
const port = portMatch ? parseInt(portMatch[1], 10) : 3000;
|
|
105
|
+
return JSON.stringify({ port });
|
|
106
|
+
},
|
|
107
|
+
} as IAgentRuntime;
|
|
108
|
+
|
|
109
|
+
// Initialize service
|
|
110
|
+
service = new NgrokService(runtime);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
afterAll(async () => {
|
|
114
|
+
if (skipTests) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Clean up any active tunnels
|
|
119
|
+
if (service?.isActive()) {
|
|
120
|
+
await service.stopTunnel();
|
|
121
|
+
await testDelay(testConfig.ngrok.stopWaitTime);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Stop test server
|
|
125
|
+
if (testServer) {
|
|
126
|
+
await new Promise<void>((resolve) => {
|
|
127
|
+
testServer.close(() => resolve());
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
beforeEach(async () => {
|
|
133
|
+
if (skipTests) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Add delay before each test
|
|
138
|
+
await testDelay();
|
|
139
|
+
|
|
140
|
+
// Ensure clean state before each test
|
|
141
|
+
if (service?.isActive()) {
|
|
142
|
+
await service.stopTunnel();
|
|
143
|
+
await testDelay(testConfig.ngrok.stopWaitTime);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
afterEach(async () => {
|
|
148
|
+
if (skipTests) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Ensure tunnel is stopped after each test
|
|
153
|
+
if (service?.isActive()) {
|
|
154
|
+
await service.stopTunnel();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Add delay after each test
|
|
158
|
+
await testDelay();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Helper function for making requests through ngrok
|
|
162
|
+
async function fetchWithNgrokHeaders(url: string, options?: RequestInit): Promise<Response> {
|
|
163
|
+
return fetch(url, {
|
|
164
|
+
...options,
|
|
165
|
+
headers: {
|
|
166
|
+
...options?.headers,
|
|
167
|
+
'ngrok-skip-browser-warning': 'true',
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
describe('Basic Tunnel Operations', () => {
|
|
173
|
+
it(
|
|
174
|
+
'should start a tunnel with real ngrok',
|
|
175
|
+
async () => {
|
|
176
|
+
if (skipTests) {
|
|
177
|
+
console.log('Test skipped - no auth token');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const url = await service.startTunnel(testServerPort);
|
|
182
|
+
|
|
183
|
+
expect(url).toBeTruthy();
|
|
184
|
+
expect(url).toMatch(/^https:\/\/[a-zA-Z0-9-]+\.ngrok(-free)?\.app$/);
|
|
185
|
+
expect(service.isActive()).toBe(true);
|
|
186
|
+
|
|
187
|
+
const status = service.getStatus();
|
|
188
|
+
expect(status.active).toBe(true);
|
|
189
|
+
expect(status.url).toBe(url as string | null);
|
|
190
|
+
expect(status.port).toBe(testServerPort);
|
|
191
|
+
expect(status.provider).toBe('ngrok');
|
|
192
|
+
|
|
193
|
+
console.log(`✅ Tunnel created: ${url}`);
|
|
194
|
+
},
|
|
195
|
+
testConfig.execution.e2eTimeout
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
it(
|
|
199
|
+
'should stop a tunnel',
|
|
200
|
+
async () => {
|
|
201
|
+
if (skipTests) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Start tunnel first
|
|
206
|
+
const _url = await service.startTunnel(testServerPort);
|
|
207
|
+
expect(service.isActive()).toBe(true);
|
|
208
|
+
|
|
209
|
+
// Stop tunnel
|
|
210
|
+
await service.stopTunnel();
|
|
211
|
+
expect(service.isActive()).toBe(false);
|
|
212
|
+
expect(service.getUrl()).toBeNull();
|
|
213
|
+
|
|
214
|
+
const status = service.getStatus();
|
|
215
|
+
expect(status.active).toBe(false);
|
|
216
|
+
expect(status.url).toBeNull();
|
|
217
|
+
expect(status.port).toBeNull();
|
|
218
|
+
},
|
|
219
|
+
testConfig.execution.e2eTimeout
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
it(
|
|
223
|
+
'should handle multiple start/stop cycles',
|
|
224
|
+
async () => {
|
|
225
|
+
if (skipTests) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
for (let i = 0; i < 3; i++) {
|
|
230
|
+
console.log(`\n🔄 Cycle ${i + 1}/3`);
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const url = await service.startTunnel(testServerPort);
|
|
234
|
+
expect(url).toBeTruthy();
|
|
235
|
+
expect(service.isActive()).toBe(true);
|
|
236
|
+
|
|
237
|
+
await service.stopTunnel();
|
|
238
|
+
expect(service.isActive()).toBe(false);
|
|
239
|
+
|
|
240
|
+
// Use enhanced delay between cycles to ensure ngrok API is ready
|
|
241
|
+
if (i < 2) {
|
|
242
|
+
console.log('⏳ Waiting for ngrok to fully clean up...');
|
|
243
|
+
await ngrokSafeDelay(testConfig.ngrok.minIntervalBetweenStarts + 2000);
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.error(
|
|
247
|
+
`❌ Cycle ${i + 1} failed:`,
|
|
248
|
+
error instanceof Error ? error.message : String(error)
|
|
249
|
+
);
|
|
250
|
+
// If we get an error, wait extra time before continuing
|
|
251
|
+
if (i < 2) {
|
|
252
|
+
console.log('⏳ Extra wait after error...');
|
|
253
|
+
await ngrokSafeDelay(5000);
|
|
254
|
+
}
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
testConfig.execution.e2eTimeout * 2
|
|
260
|
+
); // Double timeout for this test
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('Action Integration Tests', () => {
|
|
264
|
+
it('should start tunnel via action', async () => {
|
|
265
|
+
if (skipTests) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const message: Memory = {
|
|
270
|
+
id: '00000000-0000-0000-0000-000000000001' as `${string}-${string}-${string}-${string}-${string}`,
|
|
271
|
+
agentId: runtime.agentId,
|
|
272
|
+
roomId:
|
|
273
|
+
'00000000-0000-0000-0000-000000000003' as `${string}-${string}-${string}-${string}-${string}`,
|
|
274
|
+
content: { text: `start tunnel on port ${testServerPort}` },
|
|
275
|
+
createdAt: Date.now(),
|
|
276
|
+
} as Memory;
|
|
277
|
+
|
|
278
|
+
const callback = jest.fn().mockResolvedValue([]);
|
|
279
|
+
const result = await startTunnelAction.handler(runtime, message, {} as State, {}, callback);
|
|
280
|
+
|
|
281
|
+
expect(result).toBeDefined();
|
|
282
|
+
expect(typeof result).toBe('object');
|
|
283
|
+
expect(callback).toHaveBeenCalledWith({
|
|
284
|
+
text: expect.stringContaining('started successfully'),
|
|
285
|
+
metadata: {
|
|
286
|
+
action: 'tunnel_started',
|
|
287
|
+
tunnelUrl: expect.stringMatching(/^https:\/\/[a-zA-Z0-9-]+\.ngrok(-free)?\.app$/),
|
|
288
|
+
port: testServerPort,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(service.isActive()).toBe(true);
|
|
293
|
+
}, 30000);
|
|
294
|
+
|
|
295
|
+
it('should get tunnel status via action', async () => {
|
|
296
|
+
if (skipTests) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Start tunnel first
|
|
301
|
+
await service.startTunnel(testServerPort);
|
|
302
|
+
const tunnelUrl = service.getUrl();
|
|
303
|
+
|
|
304
|
+
const message: Memory = {
|
|
305
|
+
id: '00000000-0000-0000-0000-000000000001' as `${string}-${string}-${string}-${string}-${string}`,
|
|
306
|
+
agentId: runtime.agentId,
|
|
307
|
+
roomId:
|
|
308
|
+
'00000000-0000-0000-0000-000000000003' as `${string}-${string}-${string}-${string}-${string}`,
|
|
309
|
+
content: { text: 'tunnel status' },
|
|
310
|
+
createdAt: Date.now(),
|
|
311
|
+
} as Memory;
|
|
312
|
+
|
|
313
|
+
const callback = jest.fn().mockResolvedValue([]);
|
|
314
|
+
const result = await getTunnelStatusAction.handler(
|
|
315
|
+
runtime,
|
|
316
|
+
message,
|
|
317
|
+
{} as State,
|
|
318
|
+
{},
|
|
319
|
+
callback
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
expect(result).toBeDefined();
|
|
323
|
+
expect(typeof result).toBe('object');
|
|
324
|
+
expect(callback).toHaveBeenCalledWith({
|
|
325
|
+
text: expect.stringContaining('Ngrok tunnel is active'),
|
|
326
|
+
metadata: expect.objectContaining({
|
|
327
|
+
action: 'tunnel_status',
|
|
328
|
+
active: true,
|
|
329
|
+
url: tunnelUrl,
|
|
330
|
+
port: testServerPort,
|
|
331
|
+
uptime: expect.any(String),
|
|
332
|
+
provider: 'ngrok',
|
|
333
|
+
}),
|
|
334
|
+
});
|
|
335
|
+
}, 30000);
|
|
336
|
+
|
|
337
|
+
it('should stop tunnel via action', async () => {
|
|
338
|
+
if (skipTests) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Start tunnel first
|
|
343
|
+
const url = await service.startTunnel(testServerPort);
|
|
344
|
+
expect(service.isActive()).toBe(true);
|
|
345
|
+
|
|
346
|
+
const message: Memory = {
|
|
347
|
+
id: '00000000-0000-0000-0000-000000000001' as `${string}-${string}-${string}-${string}-${string}`,
|
|
348
|
+
agentId: runtime.agentId,
|
|
349
|
+
roomId:
|
|
350
|
+
'00000000-0000-0000-0000-000000000003' as `${string}-${string}-${string}-${string}-${string}`,
|
|
351
|
+
content: { text: 'stop tunnel' },
|
|
352
|
+
createdAt: Date.now(),
|
|
353
|
+
} as Memory;
|
|
354
|
+
|
|
355
|
+
const callback = jest.fn().mockResolvedValue([]);
|
|
356
|
+
const result = await stopTunnelAction.handler(runtime, message, {} as State, {}, callback);
|
|
357
|
+
|
|
358
|
+
expect(result).toBeDefined();
|
|
359
|
+
expect(typeof result).toBe('object');
|
|
360
|
+
expect(callback).toHaveBeenCalledWith(
|
|
361
|
+
expect.objectContaining({
|
|
362
|
+
text: expect.stringContaining('stopped successfully'),
|
|
363
|
+
metadata: expect.objectContaining({
|
|
364
|
+
previousUrl: url,
|
|
365
|
+
previousPort: testServerPort,
|
|
366
|
+
}),
|
|
367
|
+
})
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
expect(service.isActive()).toBe(false);
|
|
371
|
+
}, 30000);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe('Real HTTP Traffic Tests', () => {
|
|
375
|
+
it('should handle real HTTP requests through tunnel', async () => {
|
|
376
|
+
if (skipTests) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const tunnelUrl = (await service.startTunnel(testServerPort)) as string;
|
|
381
|
+
|
|
382
|
+
// Test health endpoint
|
|
383
|
+
const healthResponse = await fetchWithNgrokHeaders(`${tunnelUrl}/health`);
|
|
384
|
+
const healthData = await healthResponse.json();
|
|
385
|
+
|
|
386
|
+
expect(healthResponse.status).toBe(200);
|
|
387
|
+
expect(healthData.status).toBe('healthy');
|
|
388
|
+
expect(healthData.timestamp).toBeTruthy();
|
|
389
|
+
}, 30000);
|
|
390
|
+
|
|
391
|
+
it('should handle webhook requests through tunnel', async () => {
|
|
392
|
+
if (skipTests) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const tunnelUrl = (await service.startTunnel(testServerPort)) as string;
|
|
397
|
+
|
|
398
|
+
// Send webhook request
|
|
399
|
+
const webhookPayload = {
|
|
400
|
+
event: 'test.webhook',
|
|
401
|
+
timestamp: new Date().toISOString(),
|
|
402
|
+
data: {
|
|
403
|
+
message: 'Hello from ngrok tunnel',
|
|
404
|
+
testId: Math.random().toString(36),
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const response = await fetchWithNgrokHeaders(`${tunnelUrl}/webhook`, {
|
|
409
|
+
method: 'POST',
|
|
410
|
+
headers: {
|
|
411
|
+
'Content-Type': 'application/json',
|
|
412
|
+
'X-Webhook-Signature': 'test-signature',
|
|
413
|
+
},
|
|
414
|
+
body: JSON.stringify(webhookPayload),
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const responseData = await response.json();
|
|
418
|
+
|
|
419
|
+
expect(response.status).toBe(200);
|
|
420
|
+
expect(responseData.received).toBe(true);
|
|
421
|
+
expect(responseData.method).toBe('POST');
|
|
422
|
+
expect(responseData.headers['x-webhook-signature']).toBe('test-signature');
|
|
423
|
+
expect(responseData.body).toEqual(webhookPayload);
|
|
424
|
+
}, 30000);
|
|
425
|
+
|
|
426
|
+
it('should handle multiple concurrent requests', async () => {
|
|
427
|
+
if (skipTests) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const tunnelUrl = (await service.startTunnel(testServerPort)) as string;
|
|
432
|
+
|
|
433
|
+
// Send 10 concurrent requests
|
|
434
|
+
const requests = Array.from({ length: 10 }, async (_, i) => {
|
|
435
|
+
const response = await fetchWithNgrokHeaders(`${tunnelUrl}/echo`, {
|
|
436
|
+
method: 'POST',
|
|
437
|
+
headers: { 'Content-Type': 'application/json' },
|
|
438
|
+
body: JSON.stringify({ requestId: i, timestamp: Date.now() }),
|
|
439
|
+
});
|
|
440
|
+
return response.json();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const responses = await Promise.all(requests);
|
|
444
|
+
|
|
445
|
+
expect(responses).toHaveLength(10);
|
|
446
|
+
responses.forEach((response, i) => {
|
|
447
|
+
expect(response.requestId).toBe(i);
|
|
448
|
+
expect(response.timestamp).toBeTruthy();
|
|
449
|
+
});
|
|
450
|
+
}, 30000);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
describe('Error Handling with Real API', () => {
|
|
454
|
+
it('should handle port already in use', async () => {
|
|
455
|
+
if (skipTests) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Start first tunnel
|
|
460
|
+
const url1 = await service.startTunnel(testServerPort);
|
|
461
|
+
expect(url1).toBeTruthy();
|
|
462
|
+
|
|
463
|
+
// Try to start another tunnel on same port (should replace)
|
|
464
|
+
const url2 = await service.startTunnel(testServerPort);
|
|
465
|
+
expect(url2).toBeTruthy();
|
|
466
|
+
expect(url2).toBe(url1); // Should be the same tunnel
|
|
467
|
+
}, 30000);
|
|
468
|
+
|
|
469
|
+
it('should handle invalid port gracefully', async () => {
|
|
470
|
+
if (skipTests) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
// Use port 0 which is reserved and invalid for ngrok
|
|
476
|
+
await service.startTunnel(0);
|
|
477
|
+
throw new Error('Should have thrown an error');
|
|
478
|
+
} catch (error) {
|
|
479
|
+
expect(error).toBeTruthy();
|
|
480
|
+
expect(service.isActive()).toBe(false);
|
|
481
|
+
}
|
|
482
|
+
}, 30000);
|
|
483
|
+
|
|
484
|
+
it('should recover from network interruption', async () => {
|
|
485
|
+
if (skipTests) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Start tunnel
|
|
490
|
+
const url = await service.startTunnel(testServerPort);
|
|
491
|
+
expect(url).toBeTruthy();
|
|
492
|
+
|
|
493
|
+
// Stop and wait for cleanup
|
|
494
|
+
await service.stopTunnel();
|
|
495
|
+
console.log('⏳ Waiting for ngrok to fully stop...');
|
|
496
|
+
await ngrokSafeDelay(3000); // Increased delay with API check
|
|
497
|
+
|
|
498
|
+
// Restart tunnel
|
|
499
|
+
console.log('🔄 Restarting tunnel...');
|
|
500
|
+
const newUrl = await service.startTunnel(testServerPort);
|
|
501
|
+
expect(newUrl).toBeTruthy();
|
|
502
|
+
expect(service.isActive()).toBe(true);
|
|
503
|
+
}, 30000);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
describe('Slack Agent Use Cases', () => {
|
|
507
|
+
it('should simulate Slack webhook events', async () => {
|
|
508
|
+
if (skipTests) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const tunnelUrl = (await service.startTunnel(testServerPort)) as string;
|
|
513
|
+
|
|
514
|
+
// Simulate Slack event
|
|
515
|
+
const slackEvent = {
|
|
516
|
+
token: 'test-token',
|
|
517
|
+
team_id: 'T1234567890',
|
|
518
|
+
event: {
|
|
519
|
+
type: 'message',
|
|
520
|
+
channel: 'C1234567890',
|
|
521
|
+
user: 'U1234567890',
|
|
522
|
+
text: 'Hello from Slack!',
|
|
523
|
+
ts: '1234567890.123456',
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const response = await fetchWithNgrokHeaders(`${tunnelUrl}/webhook`, {
|
|
528
|
+
method: 'POST',
|
|
529
|
+
headers: {
|
|
530
|
+
'Content-Type': 'application/json',
|
|
531
|
+
'X-Slack-Request-Timestamp': Date.now().toString(),
|
|
532
|
+
'X-Slack-Signature': 'v0=test-signature',
|
|
533
|
+
},
|
|
534
|
+
body: JSON.stringify(slackEvent),
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
expect(response.status).toBe(200);
|
|
538
|
+
const data = await response.json();
|
|
539
|
+
expect(data.received).toBe(true);
|
|
540
|
+
expect(data.body.event.text).toBe('Hello from Slack!');
|
|
541
|
+
}, 30000);
|
|
542
|
+
});
|
|
543
|
+
});
|