diff --git a/.env.sample b/.env.sample index 35f24371..05699699 100644 --- a/.env.sample +++ b/.env.sample @@ -108,3 +108,15 @@ DEBUG_LISTEN_TO_CONSOLE = false DEBUG_DUMPIO = false DEBUG_SLOW_MO = 0 DEBUG_DEBUGGING_PORT = 9222 + +# WEBSOCKET CONFIG +WEB_SOCKET_ENABLE = false +WEB_SOCKET_RECONNECT = false +WEB_SOCKET_REJECT_UNAUTHORIZED = false +WEB_SOCKET_PING_TIMEOUT = 16000 +WEB_SOCKET_RECONNECT_INTERVAL = 3000 +WEB_SOCKET_RECONNECT_ATTEMPTS = 3 +WEB_SOCKET_MESSAGE_INTERVAL = 3600000 +WEB_SOCKET_GATHER_ALL_OPTIONS = false +WEB_SOCKET_URL = +WEB_SOCKET_SECRET = diff --git a/CHANGELOG.md b/CHANGELOG.md index b26842be..61c9a0d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ _New Features:_ - Added the `validateOption` function for validating a single option. It is used in the code to validate individual options (`svg`, `instr`, `resources`, `customCode`, `callback`, `globalOptions`, and `themeOptions`) loaded from a file. - Added the `validateOptions` function for validating the full set of options. It is used in the code to validate options coming from functions that update global options, CLI arguments, configurations loaded via `--loadConfig`, and configurations created using the prompts functionality. - Introduced redefined `getOptions` and `updateOptions` functions to retrieve and update the original global options or a copy of global options, allowing flexibility in export scenarios. +- Introduced the ability to enable a customizable `WebSocket` connection between the Export Server instance and any server or service that supports such connections to collect chart options usage data. This is useful for gathering telemetry data. +- Added a simple filtering mechanism (based on the `./lib/schemas/telemetry.json` file) to control the data being sent. - Added a new option called `uploadLimit` to control the maximum size of a request's payload body. - Added the possibility to return a Base64 version of the chart using any export method (not only through requests). - Added support for displaying CLI usage (`-h`, `--h`, `-help`, `--help`) and version information with license details (`-v`, `--v`). @@ -135,6 +137,7 @@ _Enhancements:_ - Fixed an incorrect version change endpoint description in the `Switching Highcharts Version at Runtime` section. - Corrected example and added description of the `Node.js Module` section. - Refreshed, expanded, and completely redefined the API documentation. +- Added a `WebSocket` section containing descriptions and information about this feature. - Added a note on help and version information (`Note About Version and Help Information` section). - Added a note about path interpretation for properties requiring path settings (`Note About Paths` section). - Corrected the `Note About Chart Size` section. diff --git a/README.md b/README.md index a1df5dc0..357ae8d0 100644 --- a/README.md +++ b/README.md @@ -311,6 +311,17 @@ _Available default JSON config:_ "dumpio": false, "slowMo": 0, "debuggingPort": 9222 + }, + "webSocket": { + "enable": false, + "reconnect": false, + "rejectUnauthorized": false, + "pingTimeout": 16000, + "reconnectInterval": 3000, + "reconnectAttempts": 3, + "messageInterval": 3600000, + "gatherAllOptions": false, + "url": null } } ``` @@ -445,6 +456,19 @@ _Available environment variables:_ - `DEBUG_SLOW_MO`: Slows down Puppeteer operations by the specified number of milliseconds (defaults to `0`). - `DEBUG_DEBUGGING_PORT`: Specifies the debugging port (defaults to `9222`). +### WebSocket Config + +- `WEB_SOCKET_ENABLE`: Enables or disables the WebSocket connection (defaults to `false`). +- `WEB_SOCKET_RECONNECT`: Controls whether or not to try reconnecting to the WebSocket server in case of a disconnect (defaults to `false`). +- `WEB_SOCKET_REJECT_UNAUTHORIZED`: Determines whether the client verifies the server's SSL/TLS certificate during the handshake process (defaults to `false`). +- `WEB_SOCKET_PING_TIMEOUT`: The timeout, in milliseconds, for the heartbeat mechanism between the client and server (defaults to `16000`). +- `WEB_SOCKET_RECONNECT_INTERVAL`: The interval, in milliseconds, for the reconnect attempt (defaults to `3000`). +- `WEB_SOCKET_RECONNECT_ATTEMPTS`: The number of reconnect attempts before returning a connection error (defaults to `3`). +- `WEB_SOCKET_MESSAGE_INTERVAL`: The interval, in milliseconds, for auto sending the data through a WebSocket connection (defaults to `3600000`). +- `WEB_SOCKET_GATHER_ALL_OPTIONS`: Decides whether or not to gather all chart's options or only ones defined in the **telemetry.json** file (defaults to `false`). +- `WEB_SOCKET_URL`: The URL of the WebSocket server (defaults to ``). +- `WEB_SOCKET_SECRET`: The secret used to create a JSON Web Token sent to the WebSocket server (defaults to ``). + ## Custom JSON Config To load an additional JSON configuration file, use the `--loadConfig ` option. This JSON file can either be manually created or generated through a prompt triggered by the `--createConfig ` option. The `` value does not need a _.json_ extension, but the file's content must be valid JSON when using the `--loadConfig` option. @@ -581,6 +605,18 @@ _Available CLI arguments:_ - `--slowMo`: Slows down Puppeteer operations by the specified number of milliseconds (defaults to `0`). - `--debuggingPort`: Specifies the debugging port (defaults to `9222`). +### WebSocket Config + +- `--enableWs`: Enables or disables the WebSocket connection (defaults to `false`). +- `--wsReconnect`: Controls whether or not to try reconnecting to the WebSocket server in case of a disconnect (defaults to `false`). +- `--wsRejectUnauthorized`: Determines whether the client verifies the server's SSL/TLS certificate during the handshake process (defaults to `false`). +- `--wsPingTimeout`: The timeout, in milliseconds, for the heartbeat mechanism between the client and server (defaults to `16000`). +- `--wsReconnectInterval`: The interval, in milliseconds, for the reconnect attempt (defaults to `3000`). +- `--wsReconnectAttempts`: The number of reconnect attempts before returning a connection error (defaults to `3`). +- `--wsMessageInterval`: The interval, in milliseconds, for auto sending the data through a WebSocket connection (defaults to `3600000`). +- `--wsGatherAllOptions`: Decides whether or not to gather all chart's options or only ones defined in the **telemetry.json** file (defaults to `false`). +- `--wsUrl`: The URL of the WebSocket server (defaults to `null`). + # HTTP Server Apart from using as a CLI tool, which allows you to run one command at a time, it is also possible to configure the server to accept POST requests. The simplest way to enable the server is to run the command below: @@ -878,6 +914,30 @@ This package supports both CommonJS and ES modules. Samples and tests for every mentioned export method can be found in the `./samples` and `./tests` folders. Detailed descriptions are available in their corresponding sections on the [Wiki](https://github.com/highcharts/node-export-server/wiki). +# WebSocket + +One of the new features that v4 introduces is the ability to configure and establish a WebSocket connection between the Export Server and a user-configured WebSocket server. This can be useful for gathering telemetry data and statistics about the usage of your Export Server instance. + +## How It Works + +When enabled, a WebSocket connection will be established on Export Server startup. The WebSocket server to which the connection is made must also be configured. The authorization process is completed by sending a JWT generated based on a secret that must also be set on the WebSocket server side. + +Once the connection is established, the chart data from each request to the Export Server is passed to the telemetry module, which filters the data based on a JSON file that specifies which data needs to be sent. This file can be found under `./lib/schemas/telemetry.json` and can be modified as needed, but the proposed structure must be maintained. It is also possible to send the entire object of chart options by setting the `gatherAllOptions` option to **true**. + +Data processed in this way is saved in an object that collects information about requests for a certain period and is batch sent to the WebSocket server at specified intervals. After sending, the object is cleared of request data and gathers new data until the next interval. + +Please refer to the [Configuration](https://github.com/highcharts/node-export-server?tab=readme-ov-file#configuration) section for descriptions of the options. + +## Additional Notes + +- In order for the heartbeat mechanism between the WebSocket client and server to work correctly, the `pingTimeout` should be set to a higher value in milliseconds than its equivalent in the WebSocket server. + +- Setting **0** for any of the interval/timeout-related options (`pingTimeout`, `reconnectInterval`, or `messageInterval`) will disable that option. Bear in mind, however, that disabling `pingTimeout` might result in WebSocket clients not being terminated if no response is received from the server. + +- Disabling `pingTimeout` will also disable the reconnect mechanism. + +- When using a self-signed certificate (for example, for testing purposes), the `rejectUnauthorized` option should be disabled. Otherwise, it will result in an error and the connection to the WebSocket server will fail. + # Tips, Tricks & Notes ## Note About Version And Help Information diff --git a/lib/resourceRelease.js b/lib/resourceRelease.js index 150b217a..db901662 100644 --- a/lib/resourceRelease.js +++ b/lib/resourceRelease.js @@ -21,6 +21,7 @@ import { killPool } from './pool.js'; import { clearAllTimers } from './timer.js'; import { closeServers } from './server/server.js'; +import { terminateClients } from './server/webSocket.js'; /** * Performs cleanup operations to ensure a graceful shutdown of the process. @@ -39,6 +40,9 @@ export async function shutdownCleanUp(exitCode = 0) { // Clear all ongoing intervals clearAllTimers(), + // Terminate all connected WebSocket clients + terminateClients(), + // Get available server instances (HTTP/HTTPS) and close them closeServers(), diff --git a/lib/schemas/config.js b/lib/schemas/config.js index ee12aec4..8b9ddff8 100644 --- a/lib/schemas/config.js +++ b/lib/schemas/config.js @@ -996,6 +996,103 @@ const defaultConfig = { type: 'number' } } + }, + webSocket: { + enable: { + value: false, + types: ['boolean'], + envLink: 'WEB_SOCKET_ENABLE', + cliName: 'enableWs', + description: 'Enables or disables the WebSocket connection', + promptOptions: { + type: 'toggle' + } + }, + reconnect: { + value: false, + types: ['boolean'], + envLink: 'WEB_SOCKET_RECONNECT', + cliName: 'wsReconnect', + description: + 'Whether or not to attempt to reconnect to the WebSocket server if disconnected', + promptOptions: { + type: 'toggle' + } + }, + rejectUnauthorized: { + value: false, + types: ['boolean'], + envLink: 'WEB_SOCKET_REJECT_UNAUTHORIZED', + cliName: 'wsRejectUnauthorized', + description: + "Whether or not to client should verify the server's SSL/TLS certificate during the handshake", + promptOptions: { + type: 'toggle' + } + }, + pingTimeout: { + value: 16000, + types: ['number'], + envLink: 'WEB_SOCKET_PING_TIMEOUT', + cliName: 'wsPingTimeout', + description: + 'Timeout in milliseconds for the heartbeat mechanism between client and server', + promptOptions: { + type: 'number' + } + }, + reconnectInterval: { + value: 3000, + types: ['number'], + envLink: 'WEB_SOCKET_RECONNECT_INTERVAL', + cliName: 'wsReconnectInterval', + description: 'Interval in milliseconds between reconnect attempts', + promptOptions: { + type: 'number' + } + }, + reconnectAttempts: { + value: 3, + types: ['number'], + envLink: 'WEB_SOCKET_RECONNECT_ATTEMPTS', + cliName: 'wsReconnectAttempts', + description: 'Number of attempts to reconnect before reporting an error', + promptOptions: { + type: 'number' + } + }, + messageInterval: { + value: 3600000, + types: ['number'], + envLink: 'WEB_SOCKET_MESSAGE_INTERVAL', + cliName: 'wsMessageInterval', + description: + 'Interval in milliseconds for automatically sending data through the WebSocket connection', + promptOptions: { + type: 'number' + } + }, + gatherAllOptions: { + value: false, + types: ['boolean'], + envLink: 'WEB_SOCKET_GATHER_ALL_OPTIONS', + cliName: 'wsGatherAllOptions', + description: + 'Whether or not to gather all chart options or only those defined in the telemetry.json file', + promptOptions: { + type: 'toggle' + } + }, + url: { + value: null, + types: ['string', 'null'], + envLink: 'WEB_SOCKET_URL', + cliName: 'wsUrl', + description: 'URL of the WebSocket server', + promptOptions: { + type: 'text' + } + } } }; diff --git a/lib/schemas/telemetry.json b/lib/schemas/telemetry.json new file mode 100644 index 00000000..1526f5bd --- /dev/null +++ b/lib/schemas/telemetry.json @@ -0,0 +1,22 @@ +{ + "boost": null, + "chart": { + "type": null, + "options3d": { + "enabled": null + } + }, + "colors": null, + "legend": { + "enabled": null + }, + "series": { + "type": null + }, + "xAxis": { + "type": null + }, + "yAxis": { + "type": null + } +} diff --git a/lib/server/routes/export.js b/lib/server/routes/export.js index c664a573..6a92df59 100644 --- a/lib/server/routes/export.js +++ b/lib/server/routes/export.js @@ -23,6 +23,7 @@ See LICENSE file in root for details. import { startExport } from '../../chart.js'; import { log } from '../../logger.js'; +import { prepareTelemetry } from '../../telemetry.js'; import { getBase64, measureTime } from '../../utils.js'; import ExportError from '../../errors/ExportError.js'; @@ -106,6 +107,12 @@ async function requestExport(request, response, next) { ); } + // Telemetry only for the options based request + if (!options.export.svg) { + // Prepare and send the options through the WebSocket + prepareTelemetry(options.export.options, options.payload.requestId); + } + // Return the result in an appropriate format if (data.result) { log( diff --git a/lib/server/server.js b/lib/server/server.js index a68045ca..300a3e82 100644 --- a/lib/server/server.js +++ b/lib/server/server.js @@ -34,6 +34,8 @@ import { updateOptions } from '../config.js'; import { log, logWithStack } from '../logger.js'; import { __dirname, getAbsolutePath } from '../utils.js'; +import { webSocketInit } from './webSocket.js'; + import errorMiddleware from './middlewares/error.js'; import rateLimitingMiddleware from './middlewares/rateLimiting.js'; import validationMiddleware from './middlewares/validation.js'; @@ -160,6 +162,11 @@ export async function startServer(serverOptions = {}) { 3, `[server] Started HTTP server on ${serverOptions.host}:${serverOptions.port}.` ); + + if (activeServers.size === 1) { + // Start a WebSocket connection + webSocketInit({ ...httpServer.address(), protocol: 'http' }); + } }); } @@ -203,6 +210,11 @@ export async function startServer(serverOptions = {}) { 3, `[server] Started HTTPS server on ${serverOptions.host}:${serverOptions.ssl.port}.` ); + + if (activeServers.size === 1) { + // Start a WebSocket connection + webSocketInit({ ...httpsServer.address(), protocol: 'https' }); + } }); } } diff --git a/lib/server/webSocket.js b/lib/server/webSocket.js new file mode 100644 index 00000000..006485e5 --- /dev/null +++ b/lib/server/webSocket.js @@ -0,0 +1,311 @@ +/******************************************************************************* + +Highcharts Export Server + +Copyright (c) 2016-2025, Highsoft + +Licenced under the MIT licence. + +Additionally a valid Highcharts license is required for use. + +See LICENSE file in root for details. + +*******************************************************************************/ + +/** + * @overview Manages WebSocket connections and interactions for the Highcharts + * Export Server. Supports client reconnection, message sending, and telemetry + * data transmission. + */ + +import jwt from 'jsonwebtoken'; +import { v4 as uuid } from 'uuid'; +import WebSocket from 'ws'; + +import { getOptions } from '../config.js'; +import { log, logWithStack } from '../logger.js'; +import { telemetryData } from '../telemetry.js'; +import { addTimer } from '../timer.js'; +import { getNewDate } from '../utils.js'; +import { envs } from '../validation.js'; + +// WebSocket clients map +const webSocketClients = new Map(); + +// WebSocket options +let webSocketOptions; + +// WebSocket message sending interval +let messageInterval = null; + +/** + * Init WebSocket client and connection options. + * + * @function webSocketInit + * + * @param {Object} address - Object that contains, address and port of enabled + * HTTP or HTTPS server. + */ +export function webSocketInit(address) { + webSocketOptions = getOptions().webSocket; + if (webSocketOptions.enable === true) { + // Get the secret directly from envs + const webSocketSecret = envs.WEB_SOCKET_SECRET; + + // Options for the WebSocket connection + const connectionOptions = { + rejectUnauthorized: webSocketOptions.rejectUnauthorized, + headers: { + // Set an access token + auth: jwt.sign({ success: 'success' }, webSocketSecret, { + algorithm: 'HS256' + }), + // Send the server address in a custom header + 'X-Server-Address': `${address.protocol}://${ + ['::', '0.0.0.0'].includes(address.address) + ? 'localhost' + : address.address + }:${address.port}` + } + }; + + // Options for the WebSocket client + const clientOptions = { + id: uuid(), + reconnect: webSocketOptions.reconnect, + reconnectIntervalMs: webSocketOptions.reconnectInterval, + reconnectTry: 0, + reconnectInterval: null, + pingTimeout: null + }; + + // Start the WebSocket connection + connect(webSocketOptions.url, connectionOptions, clientOptions); + + // Start the WebSocket message sending interval, if enabled + if (webSocketOptions.messageInterval > 0) { + _sendingMessageInterval(webSocketOptions); + } + } +} + +/** + * Creates WebSocket client and connects to WebSocket server on a provided url. + * + * @function connect + * + * @param {string} webSocketUrl - The WebSocket server's URL. + * @param {Object} connectionOptions - Options for WebSocket connection. + * @param {Object} clientOptions - Options for WebSocket client. + */ +export function connect(webSocketUrl, connectionOptions, clientOptions) { + // Try to connect to indicated WebSocket server + let webSocketClient = new WebSocket(webSocketUrl, connectionOptions); + + // Open event + webSocketClient.on('open', () => { + // Not need for the reconnect interval anymore + clearInterval(clientOptions.reconnectInterval); + + // Save the client under its id + webSocketClients.set(clientOptions.id, webSocketClient); + + // Log a success message + log( + 3, + `[websocket] WebSocket: ${clientOptions.id} - Connected to server: ${webSocketUrl}.` + ); + }); + + // Close event where ping timeout is cleared + webSocketClient.on('close', (code) => { + log( + 3, + '[websocket]', + `WebSocket: ${clientOptions.id} - Disconnected from server: ${webSocketUrl} with code: ${code}.` + ); + + // Stop the heartbeat mechanism + clearTimeout(clientOptions.pingTimeout); + + // Removed client if exists + webSocketClients.delete(clientOptions.id); + webSocketClient = null; + + // Try to reconnect when enabled and if not already attempting to do so + _reconnect(webSocketUrl, connectionOptions, clientOptions); + }); + + // Error event + webSocketClient.on('error', (error) => { + logWithStack( + 1, + error, + `[websocket] WebSocket: ${clientOptions.id} - Error occured.` + ); + + // Block the reconnect mechanism when getting 403 or self-signed certificate + // error code + if ( + error.message.includes('403') || + error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT' + ) { + clientOptions.reconnect = false; + clientOptions.reconnectTry = webSocketOptions.reconnectAttempts; + } else { + // Or set the option accordingly + clientOptions.reconnect = webSocketOptions.reconnect; + } + }); + + // Message event + webSocketClient.on('message', (message) => { + log( + 3, + `[websocket] WebSocket: ${clientOptions.id} - Data received: ${message}` + ); + }); + + // The 'ping' event from a WebSocket connection with the health check + // and termination logic + webSocketClient.on('ping', () => { + log( + 3, + `[websocket] WebSocket: ${clientOptions.id} - Received PING from server: ${webSocketUrl}.` + ); + + // Only when ping timeout is not disabled + if (webSocketOptions.pingTimeout > 0) { + clearTimeout(clientOptions.pingTimeout); + clientOptions.pingTimeout = setTimeout(() => { + // Terminate the client connection + webSocketClient.terminate(); + + // Try to reconnect when enabled and if not already attempting to do so + _reconnect(webSocketUrl, connectionOptions, clientOptions); + }, webSocketOptions.pingTimeout); + } + }); +} + +/** + * Gets map of current WebSocket clients. + * + * @function getClients + * + * @param {string} id - The uuid of WebSocket client. + */ +export function getClients(id) { + return id ? webSocketClients.get(id) : webSocketClients.values(); +} + +/** + * Terminates all WebSocket clients and clear the webSocketClients map. + * + * @function terminateClients + */ +export function terminateClients() { + for (const client of webSocketClients.values()) { + client.terminate(); + } + webSocketClients.clear(); +} + +/** + * Sets up an interval to send messages through a WebSocket connection + * at a specified interval. + * + * @function _sendingMessageInterval + * + * @param {Object} webSocketOptions - Options for the WebSocket connection. + */ +function _sendingMessageInterval(webSocketOptions) { + // Set the sending message interval + messageInterval = setInterval(() => { + try { + // Get the first WebSocket client + const webSocketClient = getClients().next().value; + + // Log info about message sending process + log(3, `[websocket] WebSocket message sending queue.`); + + // If the client is found, open and there is data to send + if ( + webSocketClient && + webSocketClient.readyState === WebSocket.OPEN && + Object.keys(telemetryData.optionsPerRequest).length > 0 && + telemetryData.numberOfRequests > 0 + ) { + // Log info about message sending process + log(3, `[websocket] Sending data through a WebSocket connection.`); + + // Set the dates + const currentDate = getNewDate(); + telemetryData.lastSent = telemetryData.timeOfSent || currentDate; + telemetryData.timeOfSent = currentDate; + + // Send through the WebSocket + webSocketClient.send(JSON.stringify(telemetryData)); + + // Clear the requests number and data before collecting a new one + telemetryData.numberOfRequests = 0; + telemetryData.optionsPerRequest = {}; + } + } catch (error) { + logWithStack( + 1, + error, + `[websocket] Could not send data through WebSocket.` + ); + } + }, webSocketOptions.messageInterval); + + // Register interval for the later clearing + addTimer(messageInterval); +} + +/** + * Reconnects to WebSocket server on a provided url. + * + * @function _reconnect + * + * @param {string} webSocketUrl - The WebSocket server's URL. + * @param {Object} connectionOptions - Options for WebSocket connection. + * @param {Object} clientOptions - Options for WebSocket client. + */ +function _reconnect(webSocketUrl, connectionOptions, clientOptions) { + if ( + clientOptions.reconnect && + clientOptions.reconnectIntervalMs > 0 && + !clientOptions.reconnectInterval + ) { + // Start the reconnect interval + clientOptions.reconnectInterval = setInterval(() => { + if (clientOptions.reconnectTry < webSocketOptions.reconnectAttempts) { + log( + 3, + `[websocket] WebSocket: ${clientOptions.id} - Attempt ${++clientOptions.reconnectTry} of ${webSocketOptions.reconnectAttempts} to reconnect to server: ${webSocketUrl}.` + ); + + connect(webSocketUrl, connectionOptions, clientOptions); + } else { + clientOptions.reconnect = false; + clearInterval(clientOptions.reconnectInterval); + log( + 2, + `[websocket] WebSocket: ${clientOptions.id} - Could not reconnect to server: ${webSocketUrl}.` + ); + } + }, webSocketOptions.reconnectInterval); + + // Register interval for the later clearing + addTimer(clientOptions.reconnectInterval); + } +} + +export default { + webSocketInit, + connect, + getClients, + terminateClients +}; diff --git a/lib/telemetry.js b/lib/telemetry.js new file mode 100644 index 00000000..e927c254 --- /dev/null +++ b/lib/telemetry.js @@ -0,0 +1,131 @@ +/******************************************************************************* + +Highcharts Export Server + +Copyright (c) 2016-2025, Highsoft + +Licenced under the MIT licence. + +Additionally a valid Highcharts license is required for use. + +See LICENSE file in root for details. + +*******************************************************************************/ + +/** + * @overview Manages and filters telemetry data for chart requests + * in the Highcharts Export Server. The module reads chart options, filters them + * based on a predefined template, and stores telemetry information, such + * as the time of the request and the number of requests made. Utilizes file + * reading to load a JSON schema template and provides functions for preparing + * telemetry data and filtering chart options based on allowed properties. + */ + +import { readFileSync } from 'fs'; +import { join } from 'path'; + +import { getOptions } from './config.js'; +import { __dirname } from './utils.js'; + +// Get the telemetry template +const telemetryTemplate = JSON.parse( + readFileSync(join(__dirname, 'lib', 'schemas', 'telemetry.json')) +); + +// Possible properties in an array +const optionsInArray = ['series', 'xAxis', 'yAxis', 'zAxis']; + +// The object with telemetry data collected +export const telemetryData = { + timeOfSent: null, + lastSent: null, + optionsPerRequest: {}, + numberOfRequests: 0 +}; + +/** + * Prepares and stores telemetry data for a given request based on the chart + * options. If `gatherAllOptions` is true, all chart options are stored. + * Otherwise, only the filtered options are stored. + * + * @function prepareTelemetry + * + * @param {Object} chartOptions - The chart options to be stored or filtered. + * @param {string} requestId - The unique identifier for the current request. + */ +export function prepareTelemetry(chartOptions, requestId) { + // Save the filtered or absolute options under the request's id + telemetryData.optionsPerRequest[requestId] = getOptions().webSocket + .gatherAllOptions + ? chartOptions + : _filterData(telemetryTemplate, chartOptions); + + // Increment requests counter + telemetryData.numberOfRequests++; +} + +/** + * Recursively filters chart options based on a given template, returning only + * the necessary properties. + * + * @function _filterData + * + * @param {Object} template - The template defining the allowed properties for + * the options. + * @param {Object} options - The chart options to be filtered. + * + * @returns {Object} The filtered chart options containing only the allowed + * properties. + */ +function _filterData(template, options) { + const filteredObject = {}; + + // Cycle through allowed propeties + for (const [templateKey, templateValue] of Object.entries(template)) { + // Check if the section exists + if (options[templateKey] !== undefined) { + // Check if this is the final level of indent in the template + if (templateValue !== null) { + // Check if it is an array + if (Array.isArray(options[templateKey])) { + // And if it contains allowed properties + if (optionsInArray.includes(templateKey)) { + // Create an array + filteredObject[templateKey] = []; + // If so, cycle through all of them + for (const [index, optionsValue] of options[ + templateKey + ].entries()) { + filteredObject[templateKey][index] = _filterData( + templateValue, + optionsValue + ); + } + } else { + // Otherwise, get only the first element + filteredObject[templateKey] = _filterData( + templateValue, + options[templateKey][0] + ); + } + } else { + filteredObject[templateKey] = _filterData( + templateValue, + options[templateKey] + ); + } + } else { + // Return the option + filteredObject[templateKey] = options[templateKey]; + } + } + } + + // Return the object + return filteredObject; +} + +export default { + telemetryData, + prepareTelemetry +}; diff --git a/lib/validation.js b/lib/validation.js index b7d35b62..b3b1c5ed 100644 --- a/lib/validation.js +++ b/lib/validation.js @@ -2344,6 +2344,190 @@ export const validators = { return v.nonNegativeNum(strictCheck); }, + /** + * The `enableWs` validator that returns a Zod schema with an optional + * stricter check based on the `strictCheck` parameter. + * + * The validation schema ensures the same work as the `boolean` validator. + * + * @function enableWs + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `enableWs` + * option. + */ + enableWs(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `wsReconnect` validator that returns a Zod schema with an optional + * stricter check based on the `strictCheck` parameter. + * + * The validation schema ensures the same work as the `boolean` validator. + * + * @function wsReconnect + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `wsReconnect` + * option. + */ + wsReconnect(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `wsRejectUnauthorized` validator that returns a Zod schema with + * an optional stricter check based on the `strictCheck` parameter. + * + * The validation schema ensures the same work as the `boolean` validator. + * + * @function + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `wsRejectUnauthorized` option. + */ + wsRejectUnauthorized(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `wsPingTimeout` validator that returns a Zod schema with an optional + * stricter check based on the `strictCheck` parameter. + * + * The validation schema ensures the same work as the `nonNegativeNum` + * validator. + * + * @function wsPingTimeout + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `wsPingTimeout` option. + */ + wsPingTimeout(strictCheck) { + return v.nonNegativeNum(strictCheck); + }, + + /** + * The `wsReconnectInterval` validator that returns a Zod schema with + * an optional stricter check based on the `strictCheck` parameter. + * + * The validation schema ensures the same work as the `nonNegativeNum` + * validator. + * + * @function wsReconnectInterval + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `wsReconnectInterval` option. + */ + wsReconnectInterval(strictCheck) { + return v.nonNegativeNum(strictCheck); + }, + + /** + * The `wsReconnectAttempts` validator that returns a Zod schema with + * an optional stricter check based on the `strictCheck` parameter. + * + * The validation schema ensures the same work as the `nonNegativeNum` + * validator. + * + * @function wsReconnectAttempts + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `wsReconnectAttempts` option. + */ + wsReconnectAttempts(strictCheck) { + return v.nonNegativeNum(strictCheck); + }, + + /** + * The `wsMessageInterval` validator that returns a Zod schema with + * an optional stricter check based on the `strictCheck` parameter. + * + * The validation schema ensures the same work as the `nonNegativeNum` + * validator. + * + * @function wsMessageInterval + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `wsMessageInterval` option. + */ + wsMessageInterval(strictCheck) { + return v.nonNegativeNum(strictCheck); + }, + + /** + * The `wsGatherAllOptions` validator that returns a Zod schema with + * an optional stricter check based on the `strictCheck` parameter. + * + * The validation schema ensures the same work as the `boolean` validator. + * + * @function wsGatherAllOptions + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `wsGatherAllOptions` option. + */ + wsGatherAllOptions(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `wsUrl` validator that returns a Zod schema with an optional stricter + * check based on the `strictCheck` parameter. + * + * The validation schema ensures the same work as the `startsWith` validator. + * + * @function wsUrl + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `wsUrl` + * option. + */ + wsUrl(strictCheck) { + return v.startsWith(['ws://', 'wss://'], strictCheck).nullable(); + }, + + /** + * The `wsSecret` validator that returns a Zod schema with an optional + * stricter check based on the `strictCheck` parameter. + * + * The validation schema ensures the same work as the `string` validator. + * + * @function wsSecret + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `wsSecret` + * option. + */ + wsSecret(strictCheck) { + return v.string(strictCheck); + }, + /** * The `requestId` validator that returns a Zod schema with an optional * stricter check based on the `strictCheck` parameter. @@ -2543,6 +2727,22 @@ const DebugSchema = (strictCheck) => }) .partial(); +// Schema for the webSocket section of options +const WebSocketSchema = (strictCheck) => + z + .object({ + enable: validators.enableWs(strictCheck), + reconnect: validators.wsReconnect(strictCheck), + rejectUnauthorized: validators.wsRejectUnauthorized(strictCheck), + pingTimeout: validators.wsPingTimeout(strictCheck), + reconnectInterval: validators.wsReconnectInterval(strictCheck), + reconnectAttempts: validators.wsReconnectAttempts(strictCheck), + messageInterval: validators.wsMessageInterval(strictCheck), + gatherAllOptions: validators.wsGatherAllOptions(strictCheck), + url: validators.wsUrl(strictCheck) + }) + .partial(); + // Strict schema for the config export const StrictConfigSchema = z.object({ requestId: validators.requestId(), @@ -2555,7 +2755,8 @@ export const StrictConfigSchema = z.object({ logging: LoggingSchema(true), ui: UiSchema(true), other: OtherSchema(true), - debug: DebugSchema(true) + debug: DebugSchema(true), + webSocket: WebSocketSchema(true) }); // Loose schema for the config @@ -2570,7 +2771,8 @@ export const LooseConfigSchema = z.object({ logging: LoggingSchema(false), ui: UiSchema(false), other: OtherSchema(false), - debug: DebugSchema(false) + debug: DebugSchema(false), + webSocket: WebSocketSchema(false) }); // Schema for the environment variables config @@ -2684,7 +2886,19 @@ export const EnvSchema = z.object({ DEBUG_LISTEN_TO_CONSOLE: validators.listenToConsole(false), DEBUG_DUMPIO: validators.dumpio(false), DEBUG_SLOW_MO: validators.slowMo(false), - DEBUG_DEBUGGING_PORT: validators.debuggingPort(false) + DEBUG_DEBUGGING_PORT: validators.debuggingPort(false), + + // websocket + WEB_SOCKET_ENABLE: validators.enableWs(false), + WEB_SOCKET_RECONNECT: validators.wsReconnect(false), + WEB_SOCKET_REJECT_UNAUTHORIZED: validators.wsRejectUnauthorized(false), + WEB_SOCKET_PING_TIMEOUT: validators.wsPingTimeout(false), + WEB_SOCKET_RECONNECT_INTERVAL: validators.wsReconnectInterval(false), + WEB_SOCKET_RECONNECT_ATTEMPTS: validators.wsReconnectAttempts(false), + WEB_SOCKET_MESSAGE_INTERVAL: validators.wsMessageInterval(false), + WEB_SOCKET_GATHER_ALL_OPTIONS: validators.wsGatherAllOptions(false), + WEB_SOCKET_URL: validators.wsUrl(false), + WEB_SOCKET_SECRET: validators.wsSecret(false) }); /** diff --git a/package-lock.json b/package-lock.json index 7714c43e..a0e5694e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "express-rate-limit": "^7.5.0", "https-proxy-agent": "^7.0.6", "jsdom": "^26.0.0", + "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.1", "prompts": "^2.4.2", "puppeteer": "^24.2.0", @@ -2471,6 +2472,12 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3293,6 +3300,15 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6126,6 +6142,61 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6407,6 +6478,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6414,6 +6521,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", diff --git a/package.json b/package.json index d24cadc2..06296ba3 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "express-rate-limit": "^7.5.0", "https-proxy-agent": "^7.0.6", "jsdom": "^26.0.0", + "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.1", "prompts": "^2.4.2", "puppeteer": "^24.2.0", diff --git a/tests/unit/validation/cli.test.js b/tests/unit/validation/cli.test.js index ad92238c..c8cb774e 100644 --- a/tests/unit/validation/cli.test.js +++ b/tests/unit/validation/cli.test.js @@ -270,6 +270,19 @@ describe('CLI options should be correctly parsed and validated', () => { slowMo: 0, debuggingPort: 9222 }); + + // webSocket + tests.webSocket('webSocket', { + enable: false, + reconnect: false, + rejectUnauthorized: false, + pingTimeout: 16000, + reconnectInterval: 3000, + reconnectAttempts: 3, + messageInterval: 3600000, + gatherAllOptions: false, + url: null + }); }); describe('Puppeteer configuration options should be correctly parsed and validated', () => { @@ -659,3 +672,39 @@ describe('Debug configuration options should be correctly parsed and validated', // debug.debuggingPort tests.debugDebuggingPort('debuggingPort'); }); + +describe('WebSocket configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(LooseConfigSchema.shape.webSocket, false); + + // webSocket.enable + tests.webSocketEnable('enable'); + + // webSocket.reconnect + tests.webSocketReconnect('reconnect'); + + // webSocket.rejectUnauthorized + tests.webSocketRejectUnauthorized('rejectUnauthorized'); + + // webSocket.pingTimeout + tests.webSocketPingTimeout('pingTimeout'); + + // webSocket.reconnectInterval + tests.webSocketReconnectInterval('reconnectInterval'); + + // webSocket.reconnectAttempts + tests.webSocketReconnectAttempts('reconnectAttempts'); + + // webSocket.messageInterval + tests.webSocketMessageInterval('messageInterval'); + + // webSocket.gatherAllOptions + tests.webSocketGatherAllOptions('gatherAllOptions'); + + // webSocket.url + tests.webSocketUrl( + 'url', + ['ws://example.com', 'wss://example.com'], + ['ws:a.com', 'ws:/b.com', 'wss:c.com', 'wss:/d.com'] + ); +}); diff --git a/tests/unit/validation/config.test.js b/tests/unit/validation/config.test.js index 3d360d90..a39ff4d9 100644 --- a/tests/unit/validation/config.test.js +++ b/tests/unit/validation/config.test.js @@ -270,6 +270,19 @@ describe('Configuration options should be correctly parsed and validated', () => slowMo: 0, debuggingPort: 9222 }); + + // webSocket + tests.webSocket('webSocket', { + enable: false, + reconnect: false, + rejectUnauthorized: false, + pingTimeout: 16000, + reconnectInterval: 3000, + reconnectAttempts: 3, + messageInterval: 3600000, + gatherAllOptions: false, + url: null + }); }); describe('Puppeteer configuration options should be correctly parsed and validated', () => { @@ -670,3 +683,39 @@ describe('Debug configuration options should be correctly parsed and validated', // debug.debuggingPort tests.debugDebuggingPort('debuggingPort'); }); + +describe('WebSocket configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(StrictConfigSchema.shape.webSocket, true); + + // webSocket.enable + tests.webSocketEnable('enable'); + + // webSocket.reconnect + tests.webSocketReconnect('reconnect'); + + // webSocket.rejectUnauthorized + tests.webSocketRejectUnauthorized('rejectUnauthorized'); + + // webSocket.pingTimeout + tests.webSocketPingTimeout('pingTimeout'); + + // webSocket.reconnectInterval + tests.webSocketReconnectInterval('reconnectInterval'); + + // webSocket.reconnectAttempts + tests.webSocketReconnectAttempts('reconnectAttempts'); + + // webSocket.messageInterval + tests.webSocketMessageInterval('messageInterval'); + + // webSocket.gatherAllOptions + tests.webSocketGatherAllOptions('gatherAllOptions'); + + // webSocket.url + tests.webSocketUrl( + 'url', + ['ws://example.com', 'wss://example.com'], + ['ws:a.com', 'ws:/b.com', 'wss:c.com', 'wss:/d.com'] + ); +}); diff --git a/tests/unit/validation/envs.test.js b/tests/unit/validation/envs.test.js index 7e5cd8dc..94276bc2 100644 --- a/tests/unit/validation/envs.test.js +++ b/tests/unit/validation/envs.test.js @@ -342,3 +342,39 @@ describe('DEBUG environment variables should be correctly parsed and validated', // DEBUG_DEBUGGING_PORT tests.debugDebuggingPort('DEBUG_DEBUGGING_PORT'); }); + +describe('WEB_SOCKET environment variables should be correctly parsed and validated', () => { + // WEB_SOCKET_ENABLE + tests.webSocketEnable('WEB_SOCKET_ENABLE'); + + // WEB_SOCKET_RECONNECT + tests.webSocketReconnect('WEB_SOCKET_RECONNECT'); + + // WEB_SOCKET_REJECT_UNAUTHORIZED + tests.webSocketRejectUnauthorized('WEB_SOCKET_REJECT_UNAUTHORIZED'); + + // WEB_SOCKET_PING_TIMEOUT + tests.webSocketPingTimeout('WEB_SOCKET_PING_TIMEOUT'); + + // WEB_SOCKET_RECONNECT_INTERVAL + tests.webSocketReconnectInterval('WEB_SOCKET_RECONNECT_INTERVAL'); + + // WEB_SOCKET_RECONNECT_ATTEMPTS + tests.webSocketReconnectAttempts('WEB_SOCKET_RECONNECT_ATTEMPTS'); + + // WEB_SOCKET_MESSAGE_INTERVAL + tests.webSocketMessageInterval('WEB_SOCKET_MESSAGE_INTERVAL'); + + // WEB_SOCKET_GATHER_ALL_OPTIONS + tests.webSocketGatherAllOptions('WEB_SOCKET_GATHER_ALL_OPTIONS'); + + // WEB_SOCKET_URL + tests.webSocketUrl( + 'WEB_SOCKET_URL', + ['ws://example.com/socket', 'wss://example.com/socket'], + ['example.com', 'ws:a.com', 'ws:/b.com', 'wss:c.com', 'wss:/d.com'] + ); + + // WEB_SOCKET_SECRET + tests.webSocketSecret('WEB_SOCKET_SECRET'); +}); diff --git a/tests/unit/validation/shared.js b/tests/unit/validation/shared.js index 4edc16ab..93308038 100644 --- a/tests/unit/validation/shared.js +++ b/tests/unit/validation/shared.js @@ -2554,6 +2554,45 @@ export function configTests(schema, strictCheck) { }, debugDebuggingPort: (property) => { describe(property, () => validationTests.nonNegativeNum(property)); + }, + webSocket: (property, value) => { + describe(property, () => validationTests.configObject(property, value)); + }, + webSocketEnable: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + webSocketReconnect: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + webSocketRejectUnauthorized: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + webSocketPingTimeout: (property) => { + describe(property, () => validationTests.nonNegativeNum(property)); + }, + webSocketReconnectInterval: (property) => { + describe(property, () => validationTests.nonNegativeNum(property)); + }, + webSocketReconnectAttempts: (property) => { + describe(property, () => validationTests.nonNegativeNum(property)); + }, + webSocketMessageInterval: (property) => { + describe(property, () => validationTests.nonNegativeNum(property)); + }, + webSocketGatherAllOptions: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + webSocketUrl: (property, correctValue, incorrectValue) => { + describe(property, () => + validationTests.nullableAcceptValues( + property, + correctValue, + incorrectValue + ) + ); + }, + webSocketSecret: (property) => { + describe(property, () => validationTests.string(property, false)); } }; }