@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,521 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'bun:test';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import * as http from 'node:http';
|
|
4
|
+
import * as https from 'node:https';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import type { IAgentRuntime } from '@elizaos/core';
|
|
7
|
+
import * as dotenv from 'dotenv';
|
|
8
|
+
import { NgrokService } from '../services/NgrokService';
|
|
9
|
+
import { createMockRuntime } from './test-utils';
|
|
10
|
+
|
|
11
|
+
// Load environment variables - use correct path relative to package location
|
|
12
|
+
dotenv.config({ path: path.resolve(__dirname, '../../../../.env') });
|
|
13
|
+
|
|
14
|
+
// Helper to check if we have ngrok credentials
|
|
15
|
+
const hasNgrokCredentials = () => {
|
|
16
|
+
return !!process.env.NGROK_AUTH_TOKEN;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Helper to check if ngrok is installed
|
|
20
|
+
const isNgrokInstalled = async (): Promise<boolean> => {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const checkProcess = spawn('which', ['ngrok']);
|
|
23
|
+
checkProcess.on('exit', (code) => {
|
|
24
|
+
resolve(code === 0);
|
|
25
|
+
});
|
|
26
|
+
checkProcess.on('error', () => {
|
|
27
|
+
resolve(false);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
describe('Ngrok Integration Tests', () => {
|
|
33
|
+
let service: NgrokService;
|
|
34
|
+
let runtime: IAgentRuntime;
|
|
35
|
+
let testServer: http.Server;
|
|
36
|
+
let testServerPort: number;
|
|
37
|
+
let skipTests = false;
|
|
38
|
+
|
|
39
|
+
beforeAll(async () => {
|
|
40
|
+
// Check if we should skip tests
|
|
41
|
+
const hasAuthToken = Boolean(process.env.NGROK_AUTH_TOKEN);
|
|
42
|
+
const skipEnvVar = process.env.SKIP_NGROK_TESTS === 'true';
|
|
43
|
+
|
|
44
|
+
console.log('Ngrok integration test environment check:');
|
|
45
|
+
console.log('- NGROK_AUTH_TOKEN:', hasAuthToken ? 'Set' : 'Not set');
|
|
46
|
+
console.log('- NGROK_DOMAIN:', process.env.NGROK_DOMAIN || 'Not set');
|
|
47
|
+
console.log('- SKIP_NGROK_TESTS:', skipEnvVar);
|
|
48
|
+
|
|
49
|
+
if (!hasAuthToken || skipEnvVar) {
|
|
50
|
+
skipTests = true;
|
|
51
|
+
console.log('⚠️ Skipping ngrok integration tests');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check prerequisites
|
|
56
|
+
const ngrokInstalled = await isNgrokInstalled();
|
|
57
|
+
if (!ngrokInstalled) {
|
|
58
|
+
console.log('⚠️ Skipping integration tests - ngrok is not installed');
|
|
59
|
+
console.log(' Please install ngrok: brew install ngrok');
|
|
60
|
+
skipTests = true;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!hasNgrokCredentials()) {
|
|
65
|
+
console.log('⚠️ Running integration tests without auth token');
|
|
66
|
+
console.log(' Some features may be limited. Set NGROK_AUTH_TOKEN for full functionality');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Create mock runtime using core test-utils
|
|
70
|
+
runtime = createMockRuntime();
|
|
71
|
+
|
|
72
|
+
// Create a test HTTP server
|
|
73
|
+
testServer = http.createServer((req, res) => {
|
|
74
|
+
const body: Buffer[] = [];
|
|
75
|
+
req.on('data', (chunk) => body.push(chunk));
|
|
76
|
+
req.on('end', () => {
|
|
77
|
+
const data = Buffer.concat(body).toString();
|
|
78
|
+
|
|
79
|
+
// Echo back request details
|
|
80
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
81
|
+
res.end(
|
|
82
|
+
JSON.stringify({
|
|
83
|
+
method: req.method,
|
|
84
|
+
url: req.url,
|
|
85
|
+
headers: req.headers,
|
|
86
|
+
body: data,
|
|
87
|
+
timestamp: new Date().toISOString(),
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Start test server on random port
|
|
94
|
+
await new Promise<void>((resolve) => {
|
|
95
|
+
testServer.listen(0, () => {
|
|
96
|
+
const address = testServer.address();
|
|
97
|
+
if (address && typeof address === 'object') {
|
|
98
|
+
testServerPort = address.port;
|
|
99
|
+
}
|
|
100
|
+
console.log(`✅ Test server started on port ${testServerPort}`);
|
|
101
|
+
resolve();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
afterAll(async () => {
|
|
107
|
+
// Clean up test server
|
|
108
|
+
if (testServer) {
|
|
109
|
+
await new Promise<void>((resolve) => {
|
|
110
|
+
testServer.close(() => {
|
|
111
|
+
console.log('✅ Test server stopped');
|
|
112
|
+
resolve();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
beforeEach(() => {
|
|
119
|
+
// Create fresh service instance for each test
|
|
120
|
+
if (skipTests) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
console.log('Runtime in beforeEach:', !!runtime);
|
|
124
|
+
console.log('Runtime getSetting type:', typeof runtime?.getSetting);
|
|
125
|
+
service = new NgrokService(runtime);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
afterEach(async () => {
|
|
129
|
+
if (service?.isActive()) {
|
|
130
|
+
await service.stopTunnel();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
async function shouldSkipNgrokTest(): Promise<boolean> {
|
|
135
|
+
if (skipTests) {
|
|
136
|
+
console.log('Test skipped - ngrok credentials unavailable or tests disabled');
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (!(await isNgrokInstalled())) {
|
|
140
|
+
console.log('Skipping test: ngrok not installed');
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
describe('Basic Tunnel Operations', () => {
|
|
147
|
+
it('should start and stop a tunnel successfully', async () => {
|
|
148
|
+
if (await shouldSkipNgrokTest()) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Start tunnel
|
|
153
|
+
const url = await service.startTunnel(testServerPort);
|
|
154
|
+
|
|
155
|
+
expect(url).toBeDefined();
|
|
156
|
+
expect(url).toMatch(/^https:\/\/.*\.ngrok.*\.(io|app)$/);
|
|
157
|
+
console.log(`✅ Tunnel started: ${url}`);
|
|
158
|
+
|
|
159
|
+
// Verify tunnel is active
|
|
160
|
+
expect(service.isActive()).toBe(true);
|
|
161
|
+
expect(service.getUrl()).toBe(url as string | null);
|
|
162
|
+
|
|
163
|
+
const status = service.getStatus();
|
|
164
|
+
expect(status.active).toBe(true);
|
|
165
|
+
expect(status.port).toBe(testServerPort);
|
|
166
|
+
expect(status.url).toBe(url as string | null);
|
|
167
|
+
expect(status.startedAt).toBeInstanceOf(Date);
|
|
168
|
+
|
|
169
|
+
// Stop tunnel
|
|
170
|
+
await service.stopTunnel();
|
|
171
|
+
|
|
172
|
+
expect(service.isActive()).toBe(false);
|
|
173
|
+
expect(service.getUrl()).toBe(null);
|
|
174
|
+
console.log('✅ Tunnel stopped');
|
|
175
|
+
}, 30000); // 30 second timeout for tunnel operations
|
|
176
|
+
|
|
177
|
+
it('should handle multiple start/stop cycles', async () => {
|
|
178
|
+
if (await shouldSkipNgrokTest()) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (let i = 0; i < 3; i++) {
|
|
183
|
+
console.log(`\n🔄 Cycle ${i + 1}/3`);
|
|
184
|
+
|
|
185
|
+
const url = await service.startTunnel(testServerPort);
|
|
186
|
+
expect(url).toBeDefined();
|
|
187
|
+
expect(service.isActive()).toBe(true);
|
|
188
|
+
|
|
189
|
+
await service.stopTunnel();
|
|
190
|
+
expect(service.isActive()).toBe(false);
|
|
191
|
+
}
|
|
192
|
+
}, 60000); // 60 second timeout
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('Webhook Testing', () => {
|
|
196
|
+
it('should receive webhook calls through ngrok tunnel', async () => {
|
|
197
|
+
if (await shouldSkipNgrokTest()) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Start tunnel
|
|
202
|
+
const tunnelUrl = (await service.startTunnel(testServerPort)) as string;
|
|
203
|
+
console.log(`\n🌐 Testing webhook at: ${tunnelUrl}`);
|
|
204
|
+
|
|
205
|
+
// Test webhook with different HTTP methods
|
|
206
|
+
const testCases = [
|
|
207
|
+
{ method: 'GET', path: '/webhook/test' },
|
|
208
|
+
{ method: 'POST', path: '/webhook/event', body: { event: 'test', data: { foo: 'bar' } } },
|
|
209
|
+
{ method: 'PUT', path: '/webhook/update', body: { id: 123, status: 'active' } },
|
|
210
|
+
{ method: 'DELETE', path: '/webhook/delete/123' },
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
for (const testCase of testCases) {
|
|
214
|
+
console.log(`\n📤 Testing ${testCase.method} ${testCase.path}`);
|
|
215
|
+
|
|
216
|
+
const response = await makeHttpRequest(
|
|
217
|
+
tunnelUrl + testCase.path,
|
|
218
|
+
testCase.method,
|
|
219
|
+
testCase.body
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
expect(response.method).toBe(testCase.method);
|
|
223
|
+
expect(response.url).toBe(testCase.path);
|
|
224
|
+
|
|
225
|
+
if (testCase.body) {
|
|
226
|
+
expect(JSON.parse(response.body)).toEqual(testCase.body);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log(`✅ ${testCase.method} request successful`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Clean up
|
|
233
|
+
await service.stopTunnel();
|
|
234
|
+
}, 45000);
|
|
235
|
+
|
|
236
|
+
it('should handle concurrent webhook requests', async () => {
|
|
237
|
+
if (await shouldSkipNgrokTest()) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const tunnelUrl = (await service.startTunnel(testServerPort)) as string;
|
|
242
|
+
console.log(`\n🌐 Testing concurrent webhooks at: ${tunnelUrl}`);
|
|
243
|
+
|
|
244
|
+
// Send multiple concurrent requests
|
|
245
|
+
const requests = Array.from({ length: 10 }, (_, i) =>
|
|
246
|
+
makeHttpRequest(`${tunnelUrl}/concurrent/${i}`, 'POST', {
|
|
247
|
+
requestId: i,
|
|
248
|
+
timestamp: Date.now(),
|
|
249
|
+
})
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const responses = await Promise.all(requests);
|
|
253
|
+
|
|
254
|
+
// Verify all requests were received
|
|
255
|
+
expect(responses).toHaveLength(10);
|
|
256
|
+
responses.forEach((response, i) => {
|
|
257
|
+
expect(response.method).toBe('POST');
|
|
258
|
+
expect(response.url).toBe(`/concurrent/${i}`);
|
|
259
|
+
const body = JSON.parse(response.body);
|
|
260
|
+
expect(body.requestId).toBe(i);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
console.log('✅ All concurrent requests processed successfully');
|
|
264
|
+
|
|
265
|
+
await service.stopTunnel();
|
|
266
|
+
}, 30000);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('Error Handling', () => {
|
|
270
|
+
it('should handle tunnel start failure gracefully', async () => {
|
|
271
|
+
if (await shouldSkipNgrokTest()) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Test with port number out of valid range (> 65535)
|
|
276
|
+
// Note: NgrokService doesn't validate port numbers, it lets ngrok handle it
|
|
277
|
+
const result = await service.startTunnel(70000);
|
|
278
|
+
// The service will attempt to start with the invalid port
|
|
279
|
+
// but ngrok itself may handle it differently
|
|
280
|
+
expect(result).toBeDefined(); // Service returns a URL even if port is invalid
|
|
281
|
+
|
|
282
|
+
await service.stopTunnel();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should handle network interruptions', async () => {
|
|
286
|
+
if (await shouldSkipNgrokTest()) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const tunnelUrl = await service.startTunnel(testServerPort);
|
|
291
|
+
|
|
292
|
+
// Simulate network request with timeout
|
|
293
|
+
try {
|
|
294
|
+
await makeHttpRequest(`${tunnelUrl}/test`, 'GET', null, 5000);
|
|
295
|
+
} catch (_error) {
|
|
296
|
+
// Network errors are expected in some cases
|
|
297
|
+
console.log('⚠️ Network request failed (expected in some test scenarios)');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Tunnel should still be active
|
|
301
|
+
expect(service.isActive()).toBe(true);
|
|
302
|
+
|
|
303
|
+
await service.stopTunnel();
|
|
304
|
+
}, 30000);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe('Real-world Scenarios', () => {
|
|
308
|
+
it('should handle a simulated GitHub webhook', async () => {
|
|
309
|
+
if (await shouldSkipNgrokTest()) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const tunnelUrl = (await service.startTunnel(testServerPort)) as string;
|
|
314
|
+
console.log(`\n🐙 Simulating GitHub webhook at: ${tunnelUrl}/github/webhook`);
|
|
315
|
+
|
|
316
|
+
// Simulate GitHub push event
|
|
317
|
+
const githubPayload = {
|
|
318
|
+
ref: 'refs/heads/main',
|
|
319
|
+
repository: {
|
|
320
|
+
name: 'test-repo',
|
|
321
|
+
full_name: 'user/test-repo',
|
|
322
|
+
},
|
|
323
|
+
pusher: {
|
|
324
|
+
name: 'test-user',
|
|
325
|
+
email: 'test@example.com',
|
|
326
|
+
},
|
|
327
|
+
commits: [
|
|
328
|
+
{
|
|
329
|
+
id: 'abc123',
|
|
330
|
+
message: 'Test commit',
|
|
331
|
+
author: {
|
|
332
|
+
name: 'Test User',
|
|
333
|
+
email: 'test@example.com',
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const response = await makeHttpRequest(
|
|
340
|
+
`${tunnelUrl}/github/webhook`,
|
|
341
|
+
'POST',
|
|
342
|
+
githubPayload,
|
|
343
|
+
10000,
|
|
344
|
+
{
|
|
345
|
+
'X-GitHub-Event': 'push',
|
|
346
|
+
'X-GitHub-Delivery': 'test-delivery-id',
|
|
347
|
+
'Content-Type': 'application/json',
|
|
348
|
+
}
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
expect(response.method).toBe('POST');
|
|
352
|
+
expect(response.headers['x-github-event']).toBe('push');
|
|
353
|
+
|
|
354
|
+
const receivedPayload = JSON.parse(response.body);
|
|
355
|
+
expect(receivedPayload.repository.name).toBe('test-repo');
|
|
356
|
+
expect(receivedPayload.commits).toHaveLength(1);
|
|
357
|
+
|
|
358
|
+
console.log('✅ GitHub webhook simulation successful');
|
|
359
|
+
|
|
360
|
+
await service.stopTunnel();
|
|
361
|
+
}, 30000);
|
|
362
|
+
|
|
363
|
+
it('should handle a simulated Stripe webhook', async () => {
|
|
364
|
+
if (await shouldSkipNgrokTest()) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const tunnelUrl = (await service.startTunnel(testServerPort)) as string;
|
|
369
|
+
console.log(`\n💳 Simulating Stripe webhook at: ${tunnelUrl}/stripe/webhook`);
|
|
370
|
+
|
|
371
|
+
// Simulate Stripe payment event
|
|
372
|
+
const stripePayload = {
|
|
373
|
+
id: 'evt_test_123',
|
|
374
|
+
object: 'event',
|
|
375
|
+
type: 'payment_intent.succeeded',
|
|
376
|
+
data: {
|
|
377
|
+
object: {
|
|
378
|
+
id: 'pi_test_123',
|
|
379
|
+
amount: 2000,
|
|
380
|
+
currency: 'usd',
|
|
381
|
+
status: 'succeeded',
|
|
382
|
+
customer: 'cus_test_123',
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const response = await makeHttpRequest(
|
|
388
|
+
`${tunnelUrl}/stripe/webhook`,
|
|
389
|
+
'POST',
|
|
390
|
+
stripePayload,
|
|
391
|
+
10000,
|
|
392
|
+
{
|
|
393
|
+
'Stripe-Signature': 'test-signature',
|
|
394
|
+
'Content-Type': 'application/json',
|
|
395
|
+
}
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
expect(response.method).toBe('POST');
|
|
399
|
+
expect(response.headers['stripe-signature']).toBe('test-signature');
|
|
400
|
+
|
|
401
|
+
const receivedPayload = JSON.parse(response.body);
|
|
402
|
+
expect(receivedPayload.type).toBe('payment_intent.succeeded');
|
|
403
|
+
expect(receivedPayload.data.object.amount).toBe(2000);
|
|
404
|
+
|
|
405
|
+
console.log('✅ Stripe webhook simulation successful');
|
|
406
|
+
|
|
407
|
+
await service.stopTunnel();
|
|
408
|
+
}, 30000);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe('Performance Testing', () => {
|
|
412
|
+
it('should handle sustained traffic', async () => {
|
|
413
|
+
if (await shouldSkipNgrokTest()) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const tunnelUrl = (await service.startTunnel(testServerPort)) as string;
|
|
418
|
+
console.log(`\n📊 Performance testing at: ${tunnelUrl}`);
|
|
419
|
+
|
|
420
|
+
const startTime = Date.now();
|
|
421
|
+
const duration = 10000; // 10 seconds
|
|
422
|
+
let requestCount = 0;
|
|
423
|
+
let errorCount = 0;
|
|
424
|
+
|
|
425
|
+
// Send requests continuously for duration
|
|
426
|
+
while (Date.now() - startTime < duration) {
|
|
427
|
+
try {
|
|
428
|
+
await makeHttpRequest(
|
|
429
|
+
`${tunnelUrl}/perf/${requestCount}`,
|
|
430
|
+
'GET',
|
|
431
|
+
null,
|
|
432
|
+
1000 // 1 second timeout per request
|
|
433
|
+
);
|
|
434
|
+
requestCount++;
|
|
435
|
+
} catch (_error) {
|
|
436
|
+
errorCount++;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Small delay between requests
|
|
440
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
444
|
+
const rps = requestCount / elapsed;
|
|
445
|
+
|
|
446
|
+
console.log('\n📈 Performance Results:');
|
|
447
|
+
console.log(` - Duration: ${elapsed.toFixed(1)}s`);
|
|
448
|
+
console.log(` - Requests: ${requestCount}`);
|
|
449
|
+
console.log(` - Errors: ${errorCount}`);
|
|
450
|
+
console.log(` - RPS: ${rps.toFixed(1)}`);
|
|
451
|
+
|
|
452
|
+
expect(requestCount).toBeGreaterThan(0);
|
|
453
|
+
expect(errorCount / requestCount).toBeLessThan(0.1); // Less than 10% error rate
|
|
454
|
+
|
|
455
|
+
await service.stopTunnel();
|
|
456
|
+
}, 30000);
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Helper function to make HTTP requests
|
|
461
|
+
async function makeHttpRequest(
|
|
462
|
+
url: string,
|
|
463
|
+
method: string,
|
|
464
|
+
body: unknown,
|
|
465
|
+
timeout: number = 10000,
|
|
466
|
+
headers: Record<string, string> = {}
|
|
467
|
+
): Promise<unknown> {
|
|
468
|
+
return new Promise((resolve, reject) => {
|
|
469
|
+
const urlObj = new URL(url);
|
|
470
|
+
const requestHeaders: Record<string, string> = {
|
|
471
|
+
...headers,
|
|
472
|
+
'User-Agent': 'ElizaOS-Ngrok-Test',
|
|
473
|
+
'ngrok-skip-browser-warning': 'true',
|
|
474
|
+
};
|
|
475
|
+
const options = {
|
|
476
|
+
hostname: urlObj.hostname,
|
|
477
|
+
port: urlObj.port || 443,
|
|
478
|
+
path: urlObj.pathname + urlObj.search,
|
|
479
|
+
method,
|
|
480
|
+
headers: requestHeaders,
|
|
481
|
+
timeout,
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
if (body && method !== 'GET') {
|
|
485
|
+
const bodyStr = JSON.stringify(body);
|
|
486
|
+
requestHeaders['Content-Type'] = options.headers['Content-Type'] || 'application/json';
|
|
487
|
+
requestHeaders['Content-Length'] = Buffer.byteLength(bodyStr).toString();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const req = https.request(options, (res) => {
|
|
491
|
+
const chunks: Buffer[] = [];
|
|
492
|
+
|
|
493
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
494
|
+
res.on('end', () => {
|
|
495
|
+
try {
|
|
496
|
+
const responseBody = Buffer.concat(chunks).toString();
|
|
497
|
+
const response = JSON.parse(responseBody);
|
|
498
|
+
resolve(response);
|
|
499
|
+
} catch (error) {
|
|
500
|
+
reject(
|
|
501
|
+
new Error(
|
|
502
|
+
`Failed to parse response: ${error instanceof Error ? error.message : String(error)}`
|
|
503
|
+
)
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
req.on('error', reject);
|
|
510
|
+
req.on('timeout', () => {
|
|
511
|
+
req.destroy();
|
|
512
|
+
reject(new Error('Request timeout'));
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
if (body && method !== 'GET') {
|
|
516
|
+
req.write(JSON.stringify(body));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
req.end();
|
|
520
|
+
});
|
|
521
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export const testConfig = {
|
|
2
|
+
// Test execution configuration
|
|
3
|
+
execution: {
|
|
4
|
+
// Add delays between test suites to avoid rate limiting
|
|
5
|
+
suitesDelay: 3000, // 3 seconds between test suites
|
|
6
|
+
testsDelay: 1000, // 1 second between individual tests
|
|
7
|
+
|
|
8
|
+
// Maximum retries for flaky tests
|
|
9
|
+
maxRetries: 2,
|
|
10
|
+
|
|
11
|
+
// Timeout configurations
|
|
12
|
+
defaultTimeout: 30000,
|
|
13
|
+
integrationTimeout: 60000,
|
|
14
|
+
e2eTimeout: 90000,
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
// Ngrok specific configuration for tests
|
|
18
|
+
ngrok: {
|
|
19
|
+
// Don't use subdomains in tests (requires paid account)
|
|
20
|
+
useRandomSubdomains: false,
|
|
21
|
+
|
|
22
|
+
// Wait time after stopping tunnel before starting a new one
|
|
23
|
+
stopWaitTime: 2000,
|
|
24
|
+
|
|
25
|
+
// Rate limiting configuration
|
|
26
|
+
minIntervalBetweenStarts: 3000,
|
|
27
|
+
|
|
28
|
+
// Domain conflict retry configuration
|
|
29
|
+
domainConflictRetries: 3,
|
|
30
|
+
domainConflictBackoff: 2000,
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// Test categorization for prioritized execution
|
|
34
|
+
testPriority: {
|
|
35
|
+
// Run these test suites first (critical path)
|
|
36
|
+
high: ['unit/types.test.ts', 'unit/environment.test.ts', 'unit/plugin.test.ts'],
|
|
37
|
+
|
|
38
|
+
// Run these after high priority tests
|
|
39
|
+
medium: ['unit/actions.test.ts', 'mocks/NgrokServiceMock.ts'],
|
|
40
|
+
|
|
41
|
+
// Run these last (resource intensive)
|
|
42
|
+
low: [
|
|
43
|
+
'integration/webhook-scenarios.test.ts',
|
|
44
|
+
'e2e/real-ngrok.test.ts',
|
|
45
|
+
'ngrok-integration.test.ts',
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Helper to add delays in tests
|
|
51
|
+
export async function testDelay(ms: number = testConfig.execution.testsDelay): Promise<void> {
|
|
52
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Helper to check if we should skip ngrok tests
|
|
56
|
+
export function shouldSkipNgrokTests(): boolean {
|
|
57
|
+
const hasAuthToken = Boolean(process.env.NGROK_AUTH_TOKEN);
|
|
58
|
+
const skipTests = process.env.SKIP_NGROK_TESTS === 'true';
|
|
59
|
+
|
|
60
|
+
if (!hasAuthToken) {
|
|
61
|
+
console.log('⚠️ Skipping ngrok tests - NGROK_AUTH_TOKEN not found');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (skipTests) {
|
|
65
|
+
console.log('⚠️ Skipping ngrok tests - SKIP_NGROK_TESTS is set');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return !hasAuthToken || skipTests;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Helper to get test-specific ngrok configuration
|
|
72
|
+
export function getTestNgrokConfig() {
|
|
73
|
+
return {
|
|
74
|
+
// Don't use subdomains in tests (free account limitation)
|
|
75
|
+
NGROK_USE_RANDOM_SUBDOMAIN: 'false',
|
|
76
|
+
|
|
77
|
+
// Use the auth token from environment
|
|
78
|
+
NGROK_AUTH_TOKEN: process.env.NGROK_AUTH_TOKEN,
|
|
79
|
+
|
|
80
|
+
// Don't use fixed domain in tests - let ngrok generate random URLs
|
|
81
|
+
NGROK_DOMAIN: undefined,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as http from 'node:http';
|
|
2
|
+
|
|
3
|
+
// Helper to wait for ngrok API to be ready
|
|
4
|
+
export async function waitForNgrokAPI(maxAttempts = 10, delayMs = 500): Promise<boolean> {
|
|
5
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
6
|
+
try {
|
|
7
|
+
const isReady = await checkNgrokAPIReady();
|
|
8
|
+
if (isReady) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
} catch {
|
|
12
|
+
// API not ready yet
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (i < maxAttempts - 1) {
|
|
16
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Check if ngrok API is responding
|
|
23
|
+
function checkNgrokAPIReady(): Promise<boolean> {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const req = http.get('http://localhost:4040/api/tunnels', (res) => {
|
|
26
|
+
res.on('data', () => {}); // Consume data
|
|
27
|
+
res.on('end', () => resolve(res.statusCode === 200));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
req.on('error', () => resolve(false));
|
|
31
|
+
req.setTimeout(1000, () => {
|
|
32
|
+
req.destroy();
|
|
33
|
+
resolve(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Enhanced delay with ngrok API check
|
|
39
|
+
export async function ngrokSafeDelay(ms: number): Promise<void> {
|
|
40
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
41
|
+
// Also wait for ngrok API to be ready
|
|
42
|
+
await waitForNgrokAPI();
|
|
43
|
+
}
|