claw-dashboard 2.1.0 → 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 (90) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/docs/API.md +1 -2
  3. package/index.js +32 -42
  4. package/package.json +23 -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/src/widgets/builtin-widgets.js +0 -94
  14. package/src/widgets/index.js +0 -1
  15. package/.c8rc.json +0 -38
  16. package/.dockerignore +0 -68
  17. package/.github/dependabot.yml +0 -45
  18. package/.github/pull_request_template.md +0 -39
  19. package/.github/workflows/ci.yml +0 -147
  20. package/.github/workflows/release.yml +0 -40
  21. package/.github/workflows/security.yml +0 -84
  22. package/.husky/pre-commit +0 -1
  23. package/.planning/codebase/ARCHITECTURE.md +0 -206
  24. package/.planning/codebase/INTEGRATIONS.md +0 -150
  25. package/.planning/codebase/STACK.md +0 -122
  26. package/.planning/codebase/STRUCTURE.md +0 -201
  27. package/CONTRIBUTING.md +0 -378
  28. package/Dockerfile +0 -54
  29. package/FEATURES.md +0 -307
  30. package/TODO.md +0 -28
  31. package/ai.openclaw.dashboard.plist +0 -35
  32. package/build-cjs.js +0 -127
  33. package/cjs-shim.js +0 -13
  34. package/dist/clawdash +0 -1729
  35. package/dist/clawdash.meta.json +0 -2236
  36. package/dist/widgets.cjs +0 -6511
  37. package/docker-compose.yml +0 -67
  38. package/esbuild.config.js +0 -158
  39. package/eslint.config.js +0 -56
  40. package/examples/plugins/README.md +0 -122
  41. package/examples/plugins/api-status/index.js +0 -294
  42. package/examples/plugins/api-status/plugin.json +0 -19
  43. package/examples/plugins/hello-world/index.js +0 -104
  44. package/examples/plugins/hello-world/plugin.json +0 -15
  45. package/examples/plugins/system-metrics-chart/index.js +0 -339
  46. package/examples/plugins/system-metrics-chart/plugin.json +0 -18
  47. package/examples/plugins/weather-widget/index.js +0 -136
  48. package/examples/plugins/weather-widget/plugin.json +0 -19
  49. package/index.cjs +0 -23005
  50. package/jest.config.js +0 -11
  51. package/scripts/release.js +0 -595
  52. package/src/database.js +0 -734
  53. package/tests/alerts.test.js +0 -437
  54. package/tests/auto-save.test.js +0 -529
  55. package/tests/cache.test.js +0 -317
  56. package/tests/cli.test.js +0 -351
  57. package/tests/command-palette.test.js +0 -320
  58. package/tests/config-processor.test.js +0 -452
  59. package/tests/config-validator.test.js +0 -710
  60. package/tests/config-watcher.test.js +0 -594
  61. package/tests/config.test.js +0 -755
  62. package/tests/database.test.js +0 -438
  63. package/tests/errors.test.js +0 -189
  64. package/tests/example-plugins.test.js +0 -624
  65. package/tests/integration.test.js +0 -1321
  66. package/tests/loading-states.test.js +0 -300
  67. package/tests/manifest-validation-on-load.test.js +0 -671
  68. package/tests/performance-monitor.test.js +0 -190
  69. package/tests/plugin-api-rate-limit.test.js +0 -302
  70. package/tests/plugin-errors.test.js +0 -311
  71. package/tests/plugin-lifecycle-e2e.test.js +0 -1036
  72. package/tests/plugin-reload.test.js +0 -764
  73. package/tests/rate-limiter.test.js +0 -539
  74. package/tests/retry.test.js +0 -308
  75. package/tests/security.test.js +0 -411
  76. package/tests/settings-modal.test.js +0 -226
  77. package/tests/theme-selector.test.js +0 -57
  78. package/tests/utils.js +0 -242
  79. package/tests/utils.test.js +0 -317
  80. package/tests/validate-plugin-cli.test.js +0 -359
  81. package/tests/validation.test.js +0 -837
  82. package/tests/web-server.test.js +0 -646
  83. package/tests/widget-arrange-mode.test.js +0 -183
  84. package/tests/widget-config-hot-reload.test.js +0 -465
  85. package/tests/widget-dependency.test.js +0 -591
  86. package/tests/widget-error-boundary.test.js +0 -749
  87. package/tests/widget-error-isolation.test.js +0 -469
  88. package/tests/widget-integration.test.js +0 -1147
  89. package/tests/widget-refresh-intervals.test.js +0 -284
  90. 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
- });