claw-dashboard 2.1.1 → 2.2.0

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.
Files changed (89) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/docs/API.md +1 -2
  3. package/index.js +31 -41
  4. package/package.json +22 -12
  5. package/src/auto-save.js +11 -5
  6. package/src/cli/export-snapshot.js +3 -1
  7. package/src/cli/import-snapshot.js +3 -1
  8. package/src/config.js +9 -15
  9. package/src/errors.js +0 -9
  10. package/src/security.js +6 -2
  11. package/src/snapshot.js +73 -26
  12. package/src/web-server.js +7 -10
  13. package/.c8rc.json +0 -38
  14. package/.dockerignore +0 -68
  15. package/.github/dependabot.yml +0 -45
  16. package/.github/pull_request_template.md +0 -39
  17. package/.github/workflows/ci.yml +0 -147
  18. package/.github/workflows/release.yml +0 -40
  19. package/.github/workflows/security.yml +0 -84
  20. package/.husky/pre-commit +0 -1
  21. package/.planning/codebase/ARCHITECTURE.md +0 -206
  22. package/.planning/codebase/INTEGRATIONS.md +0 -150
  23. package/.planning/codebase/STACK.md +0 -122
  24. package/.planning/codebase/STRUCTURE.md +0 -201
  25. package/CONTRIBUTING.md +0 -378
  26. package/Dockerfile +0 -54
  27. package/FEATURES.md +0 -307
  28. package/TODO.md +0 -28
  29. package/ai.openclaw.dashboard.plist +0 -35
  30. package/build-cjs.js +0 -127
  31. package/cjs-shim.js +0 -13
  32. package/dist/clawdash +0 -1729
  33. package/dist/clawdash.meta.json +0 -2236
  34. package/dist/widgets.cjs +0 -6511
  35. package/docker-compose.yml +0 -67
  36. package/esbuild.config.js +0 -158
  37. package/eslint.config.js +0 -56
  38. package/examples/plugins/README.md +0 -122
  39. package/examples/plugins/api-status/index.js +0 -294
  40. package/examples/plugins/api-status/plugin.json +0 -19
  41. package/examples/plugins/hello-world/index.js +0 -104
  42. package/examples/plugins/hello-world/plugin.json +0 -15
  43. package/examples/plugins/system-metrics-chart/index.js +0 -339
  44. package/examples/plugins/system-metrics-chart/plugin.json +0 -18
  45. package/examples/plugins/weather-widget/index.js +0 -136
  46. package/examples/plugins/weather-widget/plugin.json +0 -19
  47. package/index.cjs +0 -23005
  48. package/jest.config.js +0 -11
  49. package/scripts/release.js +0 -595
  50. package/src/database.js +0 -734
  51. package/tests/alerts.test.js +0 -437
  52. package/tests/auto-save.test.js +0 -529
  53. package/tests/cache.test.js +0 -317
  54. package/tests/cli.test.js +0 -351
  55. package/tests/command-palette.test.js +0 -320
  56. package/tests/config-processor.test.js +0 -452
  57. package/tests/config-validator.test.js +0 -710
  58. package/tests/config-watcher.test.js +0 -594
  59. package/tests/config.test.js +0 -755
  60. package/tests/database.test.js +0 -438
  61. package/tests/errors.test.js +0 -189
  62. package/tests/example-plugins.test.js +0 -624
  63. package/tests/integration.test.js +0 -1321
  64. package/tests/loading-states.test.js +0 -300
  65. package/tests/manifest-validation-on-load.test.js +0 -671
  66. package/tests/performance-monitor.test.js +0 -190
  67. package/tests/plugin-api-rate-limit.test.js +0 -302
  68. package/tests/plugin-errors.test.js +0 -311
  69. package/tests/plugin-lifecycle-e2e.test.js +0 -1036
  70. package/tests/plugin-reload.test.js +0 -764
  71. package/tests/rate-limiter.test.js +0 -539
  72. package/tests/removed-dependencies.test.js +0 -74
  73. package/tests/retry.test.js +0 -308
  74. package/tests/security.test.js +0 -411
  75. package/tests/settings-modal.test.js +0 -226
  76. package/tests/theme-selector.test.js +0 -57
  77. package/tests/utils.js +0 -242
  78. package/tests/utils.test.js +0 -317
  79. package/tests/validate-plugin-cli.test.js +0 -359
  80. package/tests/validation.test.js +0 -837
  81. package/tests/web-server.test.js +0 -646
  82. package/tests/widget-arrange-mode.test.js +0 -183
  83. package/tests/widget-config-hot-reload.test.js +0 -465
  84. package/tests/widget-dependency.test.js +0 -591
  85. package/tests/widget-error-boundary.test.js +0 -749
  86. package/tests/widget-error-isolation.test.js +0 -469
  87. package/tests/widget-integration.test.js +0 -1147
  88. package/tests/widget-refresh-intervals.test.js +0 -284
  89. package/tests/worker-pool.test.js +0 -522
@@ -1,646 +0,0 @@
1
- /**
2
- * Tests for web server security features (rate limiting and CORS)
3
- */
4
-
5
- import http from 'http';
6
- import { WebServer, WebRateLimiter, CorsManager } from '../src/web-server.js';
7
- import config from '../src/config.js';
8
-
9
- describe('Web Server Security', () => {
10
- describe('WebRateLimiter', () => {
11
- let limiter;
12
-
13
- beforeEach(() => {
14
- limiter = new WebRateLimiter({
15
- enabled: true,
16
- windowMs: 60000,
17
- maxRequests: 5,
18
- trustProxy: false,
19
- });
20
- });
21
-
22
- afterEach(() => {
23
- limiter.stop();
24
- });
25
-
26
- describe('getClientIp', () => {
27
- test('should extract IP from socket remoteAddress', () => {
28
- const req = {
29
- socket: { remoteAddress: '192.168.1.1' },
30
- headers: {},
31
- };
32
- expect(limiter.getClientIp(req)).toBe('192.168.1.1');
33
- });
34
-
35
- test('should extract IP from connection remoteAddress', () => {
36
- const req = {
37
- connection: { remoteAddress: '192.168.1.2' },
38
- socket: null,
39
- headers: {},
40
- };
41
- expect(limiter.getClientIp(req)).toBe('192.168.1.2');
42
- });
43
-
44
- test('should use X-Forwarded-For when trustProxy is enabled', () => {
45
- limiter.trustProxy = true;
46
- const req = {
47
- socket: { remoteAddress: '192.168.1.1' },
48
- headers: { 'x-forwarded-for': '10.0.0.1, 10.0.0.2' },
49
- };
50
- expect(limiter.getClientIp(req)).toBe('10.0.0.1');
51
- });
52
-
53
- test('should use X-Real-IP when trustProxy is enabled', () => {
54
- limiter.trustProxy = true;
55
- const req = {
56
- socket: { remoteAddress: '192.168.1.1' },
57
- headers: { 'x-real-ip': '10.0.0.3' },
58
- };
59
- expect(limiter.getClientIp(req)).toBe('10.0.0.3');
60
- });
61
-
62
- test('should fall back to socket address when proxy headers missing', () => {
63
- limiter.trustProxy = true;
64
- const req = {
65
- socket: { remoteAddress: '192.168.1.1' },
66
- headers: {},
67
- };
68
- expect(limiter.getClientIp(req)).toBe('192.168.1.1');
69
- });
70
-
71
- test('should return unknown when no IP available', () => {
72
- const req = {
73
- socket: null,
74
- connection: null,
75
- headers: {},
76
- };
77
- expect(limiter.getClientIp(req)).toBe('unknown');
78
- });
79
- });
80
-
81
- describe('check', () => {
82
- test('should allow all requests when disabled', () => {
83
- limiter.enabled = false;
84
- const req = { socket: { remoteAddress: '192.168.1.1' }, headers: {} };
85
-
86
- for (let i = 0; i < 10; i++) {
87
- const result = limiter.check(req);
88
- expect(result.allowed).toBe(true);
89
- }
90
- });
91
-
92
- test('should allow requests under limit', () => {
93
- const req = { socket: { remoteAddress: '192.168.1.1' }, headers: {} };
94
-
95
- const result = limiter.check(req);
96
- expect(result.allowed).toBe(true);
97
- expect(result.remaining).toBe(5);
98
- });
99
-
100
- test('should track requests per IP separately', () => {
101
- const req1 = { socket: { remoteAddress: '192.168.1.1' }, headers: {} };
102
- const req2 = { socket: { remoteAddress: '192.168.1.2' }, headers: {} };
103
-
104
- // Fill up limiter for IP 1
105
- for (let i = 0; i < 5; i++) {
106
- limiter.record(req1);
107
- }
108
-
109
- // IP 1 should be blocked
110
- expect(limiter.check(req1).allowed).toBe(false);
111
-
112
- // IP 2 should still be allowed
113
- expect(limiter.check(req2).allowed).toBe(true);
114
- });
115
-
116
- test('should block requests over limit', () => {
117
- const req = { socket: { remoteAddress: '192.168.1.1' }, headers: {} };
118
-
119
- // Fill up limit
120
- for (let i = 0; i < 5; i++) {
121
- limiter.record(req);
122
- }
123
-
124
- const result = limiter.check(req);
125
- expect(result.allowed).toBe(false);
126
- expect(result.retryAfter).toBeGreaterThan(0);
127
- expect(result.ip).toBe('192.168.1.1');
128
- });
129
-
130
- test('should include retryAfter when blocked', () => {
131
- const req = { socket: { remoteAddress: '192.168.1.1' }, headers: {} };
132
-
133
- // Fill up limit
134
- for (let i = 0; i < 5; i++) {
135
- limiter.record(req);
136
- }
137
-
138
- const result = limiter.check(req);
139
- expect(result.retryAfter).toBeGreaterThanOrEqual(1);
140
- expect(result.retryAfter).toBeLessThanOrEqual(60);
141
- });
142
- });
143
-
144
- describe('record', () => {
145
- test('should not record when disabled', () => {
146
- limiter.enabled = false;
147
- const req = { socket: { remoteAddress: '192.168.1.1' }, headers: {} };
148
-
149
- limiter.record(req);
150
- limiter.record(req);
151
- limiter.record(req);
152
-
153
- const status = limiter.getStatus(req);
154
- expect(status.current).toBe(0);
155
- });
156
-
157
- test('should record request timestamp', () => {
158
- const req = { socket: { remoteAddress: '192.168.1.1' }, headers: {} };
159
-
160
- limiter.record(req);
161
- limiter.record(req);
162
-
163
- const status = limiter.getStatus(req);
164
- expect(status.current).toBe(2);
165
- });
166
- });
167
-
168
- describe('getStatus', () => {
169
- test('should return status when disabled', () => {
170
- limiter.enabled = false;
171
- const req = { socket: { remoteAddress: '192.168.1.1' }, headers: {} };
172
-
173
- const status = limiter.getStatus(req);
174
- expect(status.enabled).toBe(false);
175
- expect(status.limit).toBe(5);
176
- expect(status.remaining).toBe(5);
177
- expect(status.resetTime).toBe(null);
178
- });
179
-
180
- test('should return current count and remaining', () => {
181
- const req = { socket: { remoteAddress: '192.168.1.1' }, headers: {} };
182
-
183
- limiter.record(req);
184
- limiter.record(req);
185
-
186
- const status = limiter.getStatus(req);
187
- expect(status.enabled).toBe(true);
188
- expect(status.limit).toBe(5);
189
- expect(status.current).toBe(2);
190
- expect(status.remaining).toBe(3);
191
- expect(status.ip).toBe('192.168.1.1');
192
- });
193
-
194
- test('should return resetTime based on oldest request', () => {
195
- const req = { socket: { remoteAddress: '192.168.1.1' }, headers: {} };
196
-
197
- limiter.record(req);
198
-
199
- const status = limiter.getStatus(req);
200
- expect(status.resetTime).toBeTruthy();
201
- expect(new Date(status.resetTime).getTime()).toBeGreaterThan(Date.now());
202
- });
203
- });
204
-
205
- describe('cleanup', () => {
206
- test('should remove expired timestamps', async () => {
207
- limiter.windowMs = 100; // Short window for testing
208
- const req = { socket: { remoteAddress: '192.168.1.1' }, headers: {} };
209
-
210
- limiter.record(req);
211
- await new Promise(resolve => setTimeout(resolve, 150));
212
-
213
- limiter.cleanup();
214
-
215
- const status = limiter.getStatus(req);
216
- expect(status.current).toBe(0);
217
- });
218
- });
219
- });
220
-
221
- describe('CorsManager', () => {
222
- describe('isOriginAllowed', () => {
223
- test('should allow all origins with *', () => {
224
- const cors = new CorsManager({ allowedOrigins: '*' });
225
- expect(cors.isOriginAllowed('https://example.com')).toBe(true);
226
- expect(cors.isOriginAllowed('https://other.com')).toBe(true);
227
- expect(cors.isOriginAllowed(null)).toBe(true);
228
- });
229
-
230
- test('should allow exact match', () => {
231
- const cors = new CorsManager({ allowedOrigins: 'https://example.com' });
232
- expect(cors.isOriginAllowed('https://example.com')).toBe(true);
233
- expect(cors.isOriginAllowed('https://other.com')).toBe(false);
234
- });
235
-
236
- test('should allow from array of origins', () => {
237
- const cors = new CorsManager({
238
- allowedOrigins: ['https://example.com', 'https://other.com'],
239
- });
240
- expect(cors.isOriginAllowed('https://example.com')).toBe(true);
241
- expect(cors.isOriginAllowed('https://other.com')).toBe(true);
242
- expect(cors.isOriginAllowed('https://evil.com')).toBe(false);
243
- });
244
-
245
- test('should support wildcard patterns', () => {
246
- const cors = new CorsManager({
247
- allowedOrigins: ['https://*.example.com'],
248
- });
249
- expect(cors.isOriginAllowed('https://app.example.com')).toBe(true);
250
- expect(cors.isOriginAllowed('https://sub.app.example.com')).toBe(true);
251
- expect(cors.isOriginAllowed('https://other.com')).toBe(false);
252
- });
253
-
254
- test('should allow null origin (direct requests)', () => {
255
- const cors = new CorsManager({ allowedOrigins: ['https://example.com'] });
256
- expect(cors.isOriginAllowed(null)).toBe(true);
257
- expect(cors.isOriginAllowed(undefined)).toBe(true);
258
- });
259
- });
260
-
261
- describe('getHeaders', () => {
262
- test('should return correct CORS headers for allow-all', () => {
263
- const cors = new CorsManager({ allowedOrigins: '*' });
264
- const req = { headers: { origin: 'https://example.com' } };
265
- const headers = cors.getHeaders(req);
266
-
267
- expect(headers['Access-Control-Allow-Origin']).toBe('*');
268
- expect(headers['Access-Control-Allow-Methods']).toContain('GET');
269
- expect(headers['Access-Control-Allow-Methods']).toContain('POST');
270
- expect(headers['Content-Type']).toBe('application/json');
271
- });
272
-
273
- test('should mirror origin when credentials enabled with *', () => {
274
- const cors = new CorsManager({
275
- allowedOrigins: '*',
276
- credentials: true,
277
- });
278
- const req = { headers: { origin: 'https://example.com' } };
279
- const headers = cors.getHeaders(req);
280
-
281
- expect(headers['Access-Control-Allow-Origin']).toBe('https://example.com');
282
- expect(headers['Access-Control-Allow-Credentials']).toBe('true');
283
- });
284
-
285
- test('should include credentials header when enabled', () => {
286
- const cors = new CorsManager({
287
- allowedOrigins: 'https://example.com',
288
- credentials: true,
289
- });
290
- const req = { headers: { origin: 'https://example.com' } };
291
- const headers = cors.getHeaders(req);
292
-
293
- expect(headers['Access-Control-Allow-Credentials']).toBe('true');
294
- });
295
-
296
- test('should use configured methods and headers', () => {
297
- const cors = new CorsManager({
298
- allowedOrigins: '*',
299
- allowedMethods: ['GET', 'DELETE'],
300
- allowedHeaders: ['Content-Type', 'X-Custom-Header'],
301
- maxAge: 3600,
302
- });
303
- const req = { headers: {} };
304
- const headers = cors.getHeaders(req);
305
-
306
- expect(headers['Access-Control-Allow-Methods']).toBe('GET, DELETE');
307
- expect(headers['Access-Control-Allow-Headers']).toBe('Content-Type, X-Custom-Header');
308
- expect(headers['Access-Control-Max-Age']).toBe('3600');
309
- });
310
-
311
- test('should not include CORS headers for disallowed origin', () => {
312
- const cors = new CorsManager({ allowedOrigins: 'https://example.com' });
313
- const req = { headers: { origin: 'https://evil.com' } };
314
- const headers = cors.getHeaders(req);
315
-
316
- // Should not have Access-Control-Allow-Origin for disallowed origin
317
- expect(headers['Access-Control-Allow-Origin']).toBeUndefined();
318
- });
319
- });
320
- });
321
-
322
- describe('WebServer Integration', () => {
323
- let servers = [];
324
-
325
- afterEach(async () => {
326
- // Stop all servers
327
- for (const server of servers) {
328
- try {
329
- await server.stop();
330
- } catch (e) {
331
- // Ignore errors during cleanup
332
- }
333
- }
334
- servers = [];
335
- // Wait a bit for ports to be released
336
- await new Promise(resolve => setTimeout(resolve, 50));
337
- });
338
-
339
- test('should start server with rate limiting enabled', async () => {
340
- const server = new WebServer({
341
- port: 0, // Let OS assign port
342
- host: '127.0.0.1', // Bind to localhost only for tests
343
- rateLimit: {
344
- enabled: true,
345
- windowMs: 60000,
346
- maxRequests: 10,
347
- },
348
- });
349
- servers.push(server);
350
-
351
- await server.start();
352
-
353
- const info = server.getInfo();
354
- expect(info.security.rateLimit.enabled).toBe(true);
355
- expect(info.security.rateLimit.maxRequests).toBe(10);
356
- });
357
-
358
- test('should start server with restricted CORS', async () => {
359
- const server = new WebServer({
360
- port: 0,
361
- host: '127.0.0.1',
362
- corsOrigins: ['https://example.com'],
363
- corsCredentials: true,
364
- });
365
- servers.push(server);
366
-
367
- await server.start();
368
-
369
- const info = server.getInfo();
370
- expect(info.security.cors.mode).toBe('restricted');
371
- expect(info.security.cors.credentials).toBe(true);
372
- });
373
-
374
- test('should start server with allow-all CORS', async () => {
375
- const server = new WebServer({
376
- port: 0,
377
- host: '127.0.0.1',
378
- corsOrigins: '*',
379
- });
380
- servers.push(server);
381
-
382
- await server.start();
383
-
384
- const info = server.getInfo();
385
- expect(info.security.cors.mode).toBe('allow-all');
386
- });
387
-
388
- test('should track request and error counts', async () => {
389
- const server = new WebServer({
390
- port: 0,
391
- host: '127.0.0.1',
392
- rateLimit: {
393
- enabled: false, // Disable to avoid rate limiting in this test
394
- maxRequests: 5,
395
- },
396
- });
397
- servers.push(server);
398
-
399
- await server.start();
400
- const port = server.server.address().port;
401
-
402
- // Make a request to health endpoint
403
- await new Promise((resolve) => {
404
- http.get(`http://127.0.0.1:${port}/health`, (res) => {
405
- res.resume();
406
- res.on('end', resolve);
407
- }).on('error', resolve);
408
- });
409
-
410
- const info = server.getInfo();
411
- expect(info.requests).toBeGreaterThanOrEqual(1);
412
- });
413
- });
414
-
415
- describe('HTTP Endpoint Security', () => {
416
- let server;
417
- let port;
418
-
419
- beforeEach(async () => {
420
- server = new WebServer({
421
- port: 0,
422
- host: '127.0.0.1',
423
- rateLimit: {
424
- enabled: true,
425
- windowMs: 60000,
426
- maxRequests: 3,
427
- },
428
- corsOrigins: ['https://allowed.com'],
429
- });
430
- server.setDataProvider(async (type) => {
431
- if (type === 'metrics') return { cpu: 50 };
432
- if (type === 'sessions') return [];
433
- if (type === 'agents') return [];
434
- if (type === 'logs') return [];
435
- return null;
436
- });
437
-
438
- await server.start();
439
- port = server.server.address().port;
440
- });
441
-
442
- afterEach(async () => {
443
- if (server) {
444
- await server.stop();
445
- server = null;
446
- }
447
- // Wait for port release
448
- await new Promise(resolve => setTimeout(resolve, 50));
449
- });
450
-
451
- test('should return 429 when rate limit exceeded', async () => {
452
- const makeRequest = () => new Promise((resolve) => {
453
- http.get(`http://127.0.0.1:${port}/metrics`, (res) => {
454
- res.resume();
455
- resolve({ status: res.statusCode, headers: res.headers });
456
- }).on('error', () => resolve({ status: 0, headers: {} }));
457
- });
458
-
459
- // Make requests up to limit
460
- await makeRequest();
461
- await makeRequest();
462
- await makeRequest();
463
-
464
- // Next request should be rate limited
465
- const response = await makeRequest();
466
-
467
- expect(response.status).toBe(429);
468
- expect(response.headers['retry-after']).toBeDefined();
469
- expect(response.headers['x-ratelimit-remaining']).toBe('0');
470
- });
471
-
472
- test('should include rate limit headers on successful requests', async () => new Promise((resolve) => {
473
- http.get(`http://127.0.0.1:${port}/metrics`, (res) => {
474
- expect(res.statusCode).toBe(200);
475
- expect(res.headers['x-ratelimit-limit']).toBeDefined();
476
- expect(res.headers['x-ratelimit-remaining']).toBeDefined();
477
- res.resume();
478
- res.on('end', resolve);
479
- }).on('error', resolve);
480
- }));
481
-
482
- test('should return 403 for disallowed CORS origin', async () => {
483
- const options = {
484
- hostname: '127.0.0.1',
485
- port,
486
- path: '/metrics',
487
- method: 'GET',
488
- headers: {
489
- 'Origin': 'https://evil.com',
490
- },
491
- };
492
-
493
- return new Promise((resolve) => {
494
- const req = http.request(options, (res) => {
495
- expect(res.statusCode).toBe(403);
496
- res.resume();
497
- res.on('end', resolve);
498
- });
499
- req.on('error', resolve);
500
- req.end();
501
- });
502
- });
503
-
504
- test('should allow request from allowed CORS origin', async () => {
505
- const options = {
506
- hostname: '127.0.0.1',
507
- port,
508
- path: '/metrics',
509
- method: 'GET',
510
- headers: {
511
- 'Origin': 'https://allowed.com',
512
- },
513
- };
514
-
515
- return new Promise((resolve) => {
516
- const req = http.request(options, (res) => {
517
- expect(res.statusCode).toBe(200);
518
- expect(res.headers['access-control-allow-origin']).toBe('https://allowed.com');
519
- res.resume();
520
- res.on('end', resolve);
521
- });
522
- req.on('error', resolve);
523
- req.end();
524
- });
525
- });
526
-
527
- test('should handle CORS preflight requests', async () => {
528
- const options = {
529
- hostname: '127.0.0.1',
530
- port,
531
- path: '/metrics',
532
- method: 'OPTIONS',
533
- headers: {
534
- 'Origin': 'https://allowed.com',
535
- 'Access-Control-Request-Method': 'GET',
536
- },
537
- };
538
-
539
- return new Promise((resolve) => {
540
- const req = http.request(options, (res) => {
541
- expect(res.statusCode).toBe(200);
542
- expect(res.headers['access-control-allow-origin']).toBe('https://allowed.com');
543
- res.resume();
544
- res.on('end', resolve);
545
- });
546
- req.on('error', resolve);
547
- req.end();
548
- });
549
- });
550
-
551
- test('should skip rate limiting for health endpoint', async () => {
552
- // Make many requests to health endpoint
553
- for (let i = 0; i < 10; i++) {
554
- await new Promise((resolve) => {
555
- http.get(`http://127.0.0.1:${port}/health`, (res) => {
556
- expect(res.statusCode).toBe(200);
557
- res.resume();
558
- res.on('end', resolve);
559
- }).on('error', resolve);
560
- });
561
- }
562
- });
563
-
564
- test('health endpoint should include rate limit info', async () => {
565
- return new Promise((resolve) => {
566
- http.get(`http://127.0.0.1:${port}/health`, (res) => {
567
- let data = '';
568
- res.on('data', chunk => data += chunk);
569
- res.on('end', () => {
570
- const body = JSON.parse(data);
571
- expect(body.rateLimit).toBeDefined();
572
- expect(body.rateLimit.enabled).toBe(true);
573
- expect(body.rateLimit.limit).toBe(3);
574
- resolve();
575
- });
576
- }).on('error', resolve);
577
- });
578
- });
579
-
580
- test('should track requests in server info', async () => {
581
- const initialInfo = server.getInfo();
582
- const initialRequests = initialInfo.requests;
583
-
584
- // Make a request to health endpoint
585
- await new Promise((resolve) => {
586
- http.get(`http://127.0.0.1:${port}/health`, (res) => {
587
- res.resume();
588
- res.on('end', resolve);
589
- }).on('error', resolve);
590
- });
591
-
592
- const info = server.getInfo();
593
- expect(info.requests).toBeGreaterThan(initialRequests);
594
- });
595
-
596
- test('should track errors in server info', async () => {
597
- const initialInfo = server.getInfo();
598
- const initialErrors = initialInfo.errors;
599
-
600
- // Trigger a rate limit error by exceeding the limit
601
- const makeRequest = () => new Promise((resolve) => {
602
- http.get(`http://127.0.0.1:${port}/metrics`, (res) => {
603
- res.resume();
604
- res.on('end', resolve);
605
- }).on('error', resolve);
606
- });
607
-
608
- // Make requests to exceed limit (maxRequests is 3)
609
- await makeRequest();
610
- await makeRequest();
611
- await makeRequest();
612
- await makeRequest(); // This one should trigger 429 and increment errorCount
613
-
614
- const info = server.getInfo();
615
- expect(info.errors).toBeGreaterThan(initialErrors);
616
- });
617
- });
618
-
619
- describe('Configuration', () => {
620
- test('should use default config values', () => {
621
- const server = new WebServer({});
622
-
623
- expect(server.rateLimiter.enabled).toBe(config.WEB.RATE_LIMIT.ENABLED);
624
- expect(server.rateLimiter.maxRequests).toBe(config.WEB.RATE_LIMIT.MAX_REQUESTS);
625
- expect(server.corsManager.allowedOrigins).toBe(config.WEB.CORS.ALLOWED_ORIGINS);
626
- });
627
-
628
- test('should override with custom options', () => {
629
- const server = new WebServer({
630
- rateLimit: {
631
- enabled: false,
632
- windowMs: 30000,
633
- maxRequests: 50,
634
- },
635
- corsOrigins: ['https://custom.com'],
636
- corsCredentials: true,
637
- });
638
-
639
- expect(server.rateLimiter.enabled).toBe(false);
640
- expect(server.rateLimiter.windowMs).toBe(30000);
641
- expect(server.rateLimiter.maxRequests).toBe(50);
642
- expect(server.corsManager.allowedOrigins).toEqual(['https://custom.com']);
643
- expect(server.corsManager.credentials).toBe(true);
644
- });
645
- });
646
- });