@platformatic/watt-extra 0.1.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 (95) hide show
  1. package/README.md +87 -0
  2. package/app.js +124 -0
  3. package/cli.js +141 -0
  4. package/clients/compliance/compliance-types.d.ts +887 -0
  5. package/clients/compliance/compliance.mjs +1049 -0
  6. package/clients/compliance/compliance.openapi.json +6127 -0
  7. package/clients/control-plane/control-plane-types.d.ts +2696 -0
  8. package/clients/control-plane/control-plane.mjs +3051 -0
  9. package/clients/control-plane/control-plane.openapi.json +13693 -0
  10. package/clients/cron/cron-types.d.ts +1479 -0
  11. package/clients/cron/cron.mjs +872 -0
  12. package/clients/cron/cron.openapi.json +9330 -0
  13. package/compliance/index.js +21 -0
  14. package/compliance/rules/dependencies.js +76 -0
  15. package/compliance/rules/utils.js +12 -0
  16. package/eslint.config.js +11 -0
  17. package/help/start.txt +12 -0
  18. package/help/watt-extra.txt +12 -0
  19. package/index.js +45 -0
  20. package/lib/banner.js +22 -0
  21. package/lib/errors.js +34 -0
  22. package/lib/utils.js +34 -0
  23. package/lib/wattpro.js +580 -0
  24. package/package.json +50 -0
  25. package/plugins/alerts.js +115 -0
  26. package/plugins/auth.js +89 -0
  27. package/plugins/compliancy.js +70 -0
  28. package/plugins/env.js +58 -0
  29. package/plugins/flamegraphs.js +100 -0
  30. package/plugins/init.js +70 -0
  31. package/plugins/metadata.js +84 -0
  32. package/plugins/scheduler.js +48 -0
  33. package/plugins/update.js +128 -0
  34. package/renovate.json +6 -0
  35. package/test/alerts.test.js +607 -0
  36. package/test/auth.test.js +128 -0
  37. package/test/auto-cache.test.js +401 -0
  38. package/test/cli.test.js +75 -0
  39. package/test/compliancy.test.js +87 -0
  40. package/test/fixtures/runtime-domains/alpha/package.json +5 -0
  41. package/test/fixtures/runtime-domains/alpha/platformatic.json +6 -0
  42. package/test/fixtures/runtime-domains/alpha/plugin.js +16 -0
  43. package/test/fixtures/runtime-domains/beta/package.json +5 -0
  44. package/test/fixtures/runtime-domains/beta/platformatic.json +6 -0
  45. package/test/fixtures/runtime-domains/beta/plugin.js +7 -0
  46. package/test/fixtures/runtime-domains/composer/package.json +5 -0
  47. package/test/fixtures/runtime-domains/composer/platformatic.json +19 -0
  48. package/test/fixtures/runtime-domains/package.json +1 -0
  49. package/test/fixtures/runtime-domains/platformatic.json +27 -0
  50. package/test/fixtures/runtime-health/package.json +20 -0
  51. package/test/fixtures/runtime-health/platformatic.json +16 -0
  52. package/test/fixtures/runtime-health/services/service-1/package.json +17 -0
  53. package/test/fixtures/runtime-health/services/service-1/platformatic.json +16 -0
  54. package/test/fixtures/runtime-health/services/service-1/plugins/example.js +6 -0
  55. package/test/fixtures/runtime-health/services/service-1/routes/root.cjs +8 -0
  56. package/test/fixtures/runtime-health/services/service-2/package.json +17 -0
  57. package/test/fixtures/runtime-health/services/service-2/platformatic.json +16 -0
  58. package/test/fixtures/runtime-health/services/service-2/plugins/example.js +6 -0
  59. package/test/fixtures/runtime-health/services/service-2/routes/root.cjs +8 -0
  60. package/test/fixtures/runtime-next/package.json +5 -0
  61. package/test/fixtures/runtime-next/platformatic.json +9 -0
  62. package/test/fixtures/runtime-next/web/next/next.config.js +2 -0
  63. package/test/fixtures/runtime-next/web/next/package.json +7 -0
  64. package/test/fixtures/runtime-next/web/next/platformatic.json +9 -0
  65. package/test/fixtures/runtime-next/web/next/src/app/direct/route.js +3 -0
  66. package/test/fixtures/runtime-next/web/next/src/app/layout.jsx +7 -0
  67. package/test/fixtures/runtime-next/web/next/src/app/page.jsx +3 -0
  68. package/test/fixtures/runtime-scheduler/main/package.json +5 -0
  69. package/test/fixtures/runtime-scheduler/main/platformatic.json +9 -0
  70. package/test/fixtures/runtime-scheduler/main/routes/root.cjs +11 -0
  71. package/test/fixtures/runtime-scheduler/package.json +1 -0
  72. package/test/fixtures/runtime-scheduler/platformatic.json +27 -0
  73. package/test/fixtures/runtime-service/main/package.json +5 -0
  74. package/test/fixtures/runtime-service/main/platformatic.json +12 -0
  75. package/test/fixtures/runtime-service/main/routes/root.cjs +11 -0
  76. package/test/fixtures/runtime-service/package.json +1 -0
  77. package/test/fixtures/runtime-service/platformatic.json +19 -0
  78. package/test/fixtures/service-1/package.json +7 -0
  79. package/test/fixtures/service-1/platformatic.json +18 -0
  80. package/test/fixtures/service-1/routes/root.cjs +48 -0
  81. package/test/fixtures/service-2/platformatic.json +21 -0
  82. package/test/fixtures/service-2/routes/root.cjs +5 -0
  83. package/test/fixtures/service-3/package.json +5 -0
  84. package/test/fixtures/service-3/platformatic.json +21 -0
  85. package/test/fixtures/service-3/routes/root.cjs +8 -0
  86. package/test/health.test.js +44 -0
  87. package/test/helper.js +274 -0
  88. package/test/init.test.js +243 -0
  89. package/test/patch-config.test.js +434 -0
  90. package/test/scheduler.test.js +71 -0
  91. package/test/send-to-icc-retry.test.js +138 -0
  92. package/test/shared-context.test.js +82 -0
  93. package/test/spawn.test.js +110 -0
  94. package/test/trigger-flamegraphs.test.js +226 -0
  95. package/test/update.test.js +519 -0
@@ -0,0 +1,519 @@
1
+ import { test } from 'node:test'
2
+ import { equal, deepEqual } from 'node:assert'
3
+ import { WebSocketServer } from 'ws'
4
+ import { setUpEnvironment } from './helper.js'
5
+ import updatePlugin from '../plugins/update.js'
6
+ import { once, EventEmitter } from 'node:events'
7
+ import { setTimeout as sleep } from 'node:timers/promises'
8
+
9
+ function createMockApp (port) {
10
+ return {
11
+ log: {
12
+ info: () => {},
13
+ error: () => {},
14
+ warn: () => {},
15
+ debug: () => {},
16
+ },
17
+ instanceConfig: {
18
+ applicationId: 'test-application-id',
19
+ },
20
+ getAuthorizationHeader: async () => {
21
+ return { Authorization: 'Bearer test-token' }
22
+ },
23
+ env: {
24
+ PLT_APP_NAME: 'test-app',
25
+ PLT_APP_DIR: '/path/to/app',
26
+ PLT_ICC_URL: `http://localhost:${port}`,
27
+ PLT_UPDATES_RECONNECT_INTERVAL_SEC: 1,
28
+ },
29
+ }
30
+ }
31
+ const port = 13000
32
+
33
+ test('update plugin connects to websocket', async (t) => {
34
+ const ee = new EventEmitter()
35
+ setUpEnvironment()
36
+
37
+ // Setup WebSocket server
38
+ const wss = new WebSocketServer({ port })
39
+ t.after(async () => wss.close())
40
+
41
+ wss.on('connection', (ws, req) => {
42
+ equal(
43
+ req.headers.authorization,
44
+ 'Bearer test-token',
45
+ 'Should authenticate with token'
46
+ )
47
+
48
+ // Send a test subscription acknowledgment
49
+ ws.on('message', (data) => {
50
+ const message = JSON.parse(data.toString())
51
+ if (message.command === 'subscribe' && message.topic === '/config') {
52
+ ws.send(
53
+ JSON.stringify({
54
+ command: 'ack',
55
+ })
56
+ )
57
+ ee.emit('subscriptionAckSent')
58
+ }
59
+ })
60
+ })
61
+
62
+ const app = createMockApp(port)
63
+ t.after(() => app.closeUpdates())
64
+
65
+ const recordedMessages = {
66
+ info: [],
67
+ warn: [],
68
+ error: [],
69
+ }
70
+
71
+ app.log.info = (data, msg) => {
72
+ if (msg) {
73
+ recordedMessages.info.push({ data, msg })
74
+ } else {
75
+ recordedMessages.info.push(data)
76
+ }
77
+ }
78
+
79
+ await updatePlugin(app)
80
+
81
+ const ack = once(ee, 'subscriptionAckSent')
82
+ // don't await on purpose
83
+ app.connectToUpdates()
84
+ await ack
85
+
86
+ await sleep(200)
87
+
88
+ const subscriptionAckLog = recordedMessages.info.find(
89
+ (entry) =>
90
+ entry === 'Received subscription acknowledgment from updates websocket'
91
+ )
92
+ equal(!!subscriptionAckLog, true, 'Should log subscription acknowledgment')
93
+ })
94
+
95
+ test('update plugin handles config update messages', async (t) => {
96
+ const ee = new EventEmitter()
97
+ setUpEnvironment()
98
+
99
+ const wss = new WebSocketServer({ port })
100
+ t.after(async () => wss.close())
101
+
102
+ let clientSocket = null
103
+ wss.on('connection', (ws, req) => {
104
+ clientSocket = ws
105
+ equal(
106
+ req.headers.authorization,
107
+ 'Bearer test-token',
108
+ 'Should authenticate with token'
109
+ )
110
+
111
+ ws.on('message', (data) => {
112
+ const message = JSON.parse(data.toString())
113
+ if (message.command === 'subscribe' && message.topic === '/config') {
114
+ ws.send(
115
+ JSON.stringify({
116
+ command: 'ack',
117
+ })
118
+ )
119
+ ee.emit('subscriptionAckSent')
120
+ }
121
+ })
122
+ })
123
+
124
+ const app = createMockApp(port)
125
+ t.after(() => app.closeUpdates())
126
+
127
+ const loggedMessages = {
128
+ info: [],
129
+ warn: [],
130
+ error: [],
131
+ }
132
+
133
+ app.log.info = (data, msg) => {
134
+ if (msg) {
135
+ loggedMessages.info.push({ data, msg })
136
+ } else {
137
+ loggedMessages.info.push(data)
138
+ }
139
+ }
140
+
141
+ await updatePlugin(app)
142
+
143
+ // Track processed messages
144
+ const processedMessages = []
145
+ app.updateConfig = async (message) => {
146
+ processedMessages.push(message)
147
+ ee.emit('config-updated', message)
148
+ }
149
+ const ack = once(ee, 'subscriptionAckSent')
150
+ // don't await on purpose
151
+ app.connectToUpdates()
152
+ await ack
153
+
154
+ await sleep(200)
155
+
156
+ const testMessage = {
157
+ topic: '/config',
158
+ type: 'config-updated',
159
+ data: {
160
+ version: '1.0.0',
161
+ settings: {
162
+ feature1: true,
163
+ feature2: false,
164
+ },
165
+ },
166
+ }
167
+
168
+ clientSocket.send(JSON.stringify(testMessage))
169
+
170
+ const configUpdate = once(ee, 'config-updated')
171
+ await configUpdate
172
+
173
+ equal(processedMessages.length, 1)
174
+
175
+ const updatedMessage = processedMessages[0]
176
+ equal(updatedMessage.topic, '/config', 'Should receive the correct topic')
177
+ equal(
178
+ updatedMessage.type,
179
+ 'config-updated',
180
+ 'Should receive the correct type'
181
+ )
182
+ deepEqual(
183
+ updatedMessage.data,
184
+ testMessage.data,
185
+ 'Should receive the correct data'
186
+ )
187
+ })
188
+
189
+ test('update plugin ignores messages with unknown type', async (t) => {
190
+ const ee = new EventEmitter()
191
+ setUpEnvironment()
192
+
193
+ // Setup WebSocket server
194
+ const wss = new WebSocketServer({ port })
195
+ t.after(async () => wss.close())
196
+
197
+ let clientSocket = null
198
+ wss.on('connection', (ws, req) => {
199
+ clientSocket = ws
200
+ ws.on('message', (data) => {
201
+ const message = JSON.parse(data.toString())
202
+ if (message.command === 'subscribe' && message.topic === '/config') {
203
+ ws.send(
204
+ JSON.stringify({
205
+ command: 'ack',
206
+ })
207
+ )
208
+ ee.emit('subscriptionAckSent')
209
+ }
210
+ })
211
+ })
212
+
213
+ const app = createMockApp(port)
214
+ t.after(() => app.closeUpdates())
215
+
216
+ // Track processed messages
217
+ const processedMessages = []
218
+ app.updateConfig = async (message) => {
219
+ processedMessages.push(message)
220
+ ee.emit('config-updated', message)
221
+ }
222
+
223
+ const loggedMessages = {
224
+ info: [],
225
+ warn: [],
226
+ error: [],
227
+ }
228
+
229
+ app.log.info = (data, msg) => {
230
+ if (msg) {
231
+ loggedMessages.info.push({ data, msg })
232
+ } else {
233
+ loggedMessages.info.push(data)
234
+ }
235
+ }
236
+
237
+ await updatePlugin(app)
238
+ await app.connectToUpdates()
239
+
240
+ // Send a message with unknown type
241
+ const testMessage = {
242
+ topic: '/config',
243
+ type: 'unknownType',
244
+ data: { test: true },
245
+ }
246
+
247
+ clientSocket.send(JSON.stringify(testMessage))
248
+ await sleep(200)
249
+
250
+ // The message should be logged but not processed by updateConfig
251
+ equal(
252
+ processedMessages.length,
253
+ 0,
254
+ 'Should not process messages with unknown type'
255
+ )
256
+
257
+ const unknownTypeLog = loggedMessages.info.find(
258
+ (log) =>
259
+ log.data?.topic === '/config' &&
260
+ log.data?.type === 'unknownType' &&
261
+ log.msg === 'Received message, not handled type'
262
+ )
263
+
264
+ equal(
265
+ !!unknownTypeLog,
266
+ true,
267
+ 'Should log when receiving message with unknown type'
268
+ )
269
+ })
270
+
271
+ test('update plugin handles invalid messages', async (t) => {
272
+ const ee = new EventEmitter()
273
+ setUpEnvironment()
274
+
275
+ const wss = new WebSocketServer({ port })
276
+ t.after(async () => wss.close())
277
+
278
+ let clientSocket = null
279
+ wss.on('connection', (ws, req) => {
280
+ clientSocket = ws
281
+
282
+ ws.on('message', (data) => {
283
+ const message = JSON.parse(data.toString())
284
+ if (message.command === 'subscribe' && message.topic === '/config') {
285
+ ws.send(
286
+ JSON.stringify({
287
+ command: 'ack',
288
+ })
289
+ )
290
+ ee.emit('subscriptionAckSent')
291
+ }
292
+ })
293
+ })
294
+
295
+ const app = createMockApp(port)
296
+ t.after(() => app.closeUpdates())
297
+
298
+ const loggedMessages = {
299
+ info: [],
300
+ warn: [],
301
+ error: [],
302
+ }
303
+
304
+ app.log.warn = (data, msg) => {
305
+ if (msg) {
306
+ loggedMessages.warn.push({ data, msg })
307
+ } else {
308
+ loggedMessages.warn.push(data)
309
+ }
310
+ }
311
+
312
+ await updatePlugin(app)
313
+ await app.connectToUpdates()
314
+
315
+ // Send an invalid message (missing topic)
316
+ const invalidMessage = {
317
+ type: 'configUpdate',
318
+ data: { test: true },
319
+ }
320
+
321
+ clientSocket.send(JSON.stringify(invalidMessage))
322
+
323
+ await sleep(100)
324
+
325
+ const invalidMsgLog = loggedMessages.warn.find(
326
+ (log) => log.msg === 'Received invalid message from updates websocket'
327
+ )
328
+
329
+ equal(
330
+ !!invalidMsgLog,
331
+ true,
332
+ 'Should log warning when receiving invalid message'
333
+ )
334
+ })
335
+
336
+ test('update plugin handles connection errors', async (t) => {
337
+ setUpEnvironment()
338
+
339
+ const app = createMockApp(9999) // Non-existent port
340
+ t.after(() => app.closeUpdates())
341
+
342
+ const loggedErrors = []
343
+ app.log.error = (err, msg) => {
344
+ loggedErrors.push({ err, msg })
345
+ }
346
+
347
+ await updatePlugin(app)
348
+ await app.connectToUpdates()
349
+
350
+ equal(loggedErrors.length >= 1, true, 'Error should be logged')
351
+ equal(
352
+ loggedErrors[0].msg,
353
+ 'Failed to connect and subscribe to updates websocket',
354
+ 'Should log connection error'
355
+ )
356
+ })
357
+
358
+ test('update plugin closeUpdates method closes the connection', async (t) => {
359
+ const ee = new EventEmitter()
360
+ setUpEnvironment()
361
+
362
+ const closedConnections = []
363
+
364
+ const wss = new WebSocketServer({ port })
365
+ t.after(async () => wss.close())
366
+
367
+ wss.on('connection', (ws) => {
368
+ ws.on('close', () => {
369
+ closedConnections.push(ws)
370
+ ee.emit('connectionClosed')
371
+ })
372
+
373
+ ws.on('message', (data) => {
374
+ const message = JSON.parse(data.toString())
375
+ if (message.command === 'subscribe' && message.topic === '/config') {
376
+ ws.send(
377
+ JSON.stringify({
378
+ command: 'ack',
379
+ })
380
+ )
381
+ ee.emit('subscriptionAckSent')
382
+ }
383
+ })
384
+ })
385
+
386
+ const app = createMockApp(port)
387
+
388
+ await updatePlugin(app)
389
+ await app.connectToUpdates()
390
+
391
+ await app.closeUpdates()
392
+
393
+ const closeEvent = once(ee, 'connectionClosed')
394
+ await Promise.race([closeEvent, sleep(1000)])
395
+
396
+ equal(closedConnections.length, 1, 'WebSocket connection should be closed')
397
+ })
398
+
399
+ test('update plugin handles missing PLT_ICC_URL', async (t) => {
400
+ setUpEnvironment()
401
+
402
+ const app = createMockApp(port)
403
+ // Remove PLT_ICC_URL to test missing URL scenario
404
+ delete app.env.PLT_ICC_URL
405
+
406
+ const loggedMessages = {
407
+ warn: [],
408
+ }
409
+
410
+ app.log.warn = (msg) => {
411
+ loggedMessages.warn.push(msg)
412
+ }
413
+
414
+ await updatePlugin(app)
415
+ const result = await app.connectToUpdates()
416
+
417
+ equal(result, null, 'Should return null when PLT_ICC_URL is missing')
418
+ equal(loggedMessages.warn.length, 1, 'Should log a warning')
419
+ equal(
420
+ loggedMessages.warn[0],
421
+ 'No PLT_ICC_URL found in environment, cannot connect to updates websocket'
422
+ )
423
+ })
424
+
425
+ test('update plugin handles missing applicationId', async (t) => {
426
+ setUpEnvironment()
427
+
428
+ const app = createMockApp(port)
429
+ // Remove applicationId to test missing ID scenario
430
+ delete app.instanceConfig.applicationId
431
+
432
+ const loggedMessages = {
433
+ warn: [],
434
+ }
435
+
436
+ app.log.warn = (msg) => {
437
+ loggedMessages.warn.push(msg)
438
+ }
439
+
440
+ await updatePlugin(app)
441
+ const result = await app.connectToUpdates()
442
+
443
+ equal(result, null, 'Should return null when applicationId is missing')
444
+ equal(loggedMessages.warn.length, 1, 'Should log a warning')
445
+ equal(
446
+ loggedMessages.warn[0],
447
+ 'No application ID found, cannot connect to updates websocket'
448
+ )
449
+ })
450
+
451
+ test('should reconnect to updates if connection closes', async (t) => {
452
+ function startWebSocketServer () {
453
+ // Setup WebSocket server
454
+ const wss = new WebSocketServer({ port })
455
+
456
+ wss.connections = []
457
+ wss.on('connection', (ws) => {
458
+ wss.connections.push(ws)
459
+ ws.on('message', (data) => {
460
+ const message = JSON.parse(data.toString())
461
+ if (message.command === 'subscribe' && message.topic === '/config') {
462
+ ws.send(JSON.stringify({ command: 'ack' }))
463
+ }
464
+ })
465
+ })
466
+
467
+ wss.broadcast = (data) => {
468
+ wss.connections.forEach((ws) => {
469
+ ws.send(data)
470
+ })
471
+ }
472
+
473
+ return wss
474
+ }
475
+
476
+ const wss1 = startWebSocketServer()
477
+
478
+ const app = createMockApp(port)
479
+ t.after(() => app.closeUpdates())
480
+ await updatePlugin(app)
481
+
482
+ const processedMessages = []
483
+ app.updateConfig = async (message) => {
484
+ processedMessages.push(message)
485
+ }
486
+
487
+ await app.connectToUpdates()
488
+
489
+ const testMessage1 = {
490
+ topic: '/config',
491
+ type: 'config-updated',
492
+ data: { foo: 'bar' },
493
+ }
494
+
495
+ wss1.broadcast(JSON.stringify(testMessage1))
496
+ await sleep(200)
497
+
498
+ deepEqual(processedMessages, [testMessage1])
499
+
500
+ wss1.close()
501
+ for (const ws of wss1.connections) {
502
+ ws.terminate()
503
+ }
504
+
505
+ const wss2 = startWebSocketServer()
506
+ await sleep(2000)
507
+
508
+ const testMessage2 = {
509
+ topic: '/config',
510
+ type: 'config-updated',
511
+ data: { foo: 'baz' },
512
+ }
513
+
514
+ wss2.broadcast(JSON.stringify(testMessage2))
515
+ await sleep(200)
516
+
517
+ deepEqual(processedMessages, [testMessage1, testMessage2])
518
+ wss2.close()
519
+ })