diff --git a/.env.sample b/.env.sample index 02b80816..35f24371 100644 --- a/.env.sample +++ b/.env.sample @@ -98,6 +98,7 @@ OTHER_LISTEN_TO_PROCESS_EXITS = true OTHER_NO_LOGO = false OTHER_HARD_RESET_PAGE = false OTHER_BROWSER_SHELL_MODE = true +OTHER_VALIDATION = true # DEBUG CONFIG DEBUG_ENABLE = false diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fff4d22..b26842be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ _Breaking Changes:_ _New Features:_ +- Added a toggleable type validation for all options coming from various sources (environment variables, custom JSON, CLI arguments), providing a rich set of validators with both strict and loose validation. Replaced the `envs.js` module with an expanded validation logic module, `validation.js`. +- 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. - 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). @@ -27,6 +30,7 @@ _Enhancements:_ - Adjusted the options loading sequence: `default config -> environment variables` at initialization, `custom JSON -> CLI arguments` when using `setCliOptions` (CLI exports only). - The `getOptions` function can now return either a direct reference to the `globalOptions` or a copy (by setting the `getCopy` flag). - The `updateOptions` function can now update and return either a direct reference to the `globalOptions` or a copy (by setting the `getCopy` flag). +- The `updateOptions` function now validates provided options (using `validateOptions` internally) before merging them into the global options. - The `_mergeOptions` (renamed from the `mergeConfigOptions`) modifies the first object directly now and is used internally. - Replaced the fixed `absoluteProps` array (previously in `./lib/schemas/config.js`) with dynamic generation via `_createAbsoluteProps` function. - Enhanced the `isAllowedConfig` (renamed from the `isCorrectJSON`) and `_optionsStringify` functions to better handle stringified options in JSON. @@ -49,16 +53,16 @@ _Enhancements:_ - Created `_handleSize` for handling the `height`, `width`, and `scale` options. - Created `_checkDataSize` for handling the data size validation. - Optimized `initExport`, utilizing `updateOptions` for global option updates. -- The `initExport` now have its options parameters set as optional, using global option values if not provided. +- The `initExport` now have its options parameters defaulted to an empty object, using global option values if none are provided. - Updated exported API functions for module usage. - Adjusted imports to get functions from corresponding modules rather than `index.js`. - Server functions are now directly exported (rather than within a `server` object) as API functions. - The `logger` API functions that modify options now update global options. -- Added following API functions: `getOptions`, `updateOptions`, `mapToNewOptions`, `enableConsoleLogging`. +- Exposed `getOptions`, `updateOptions`, `mapToNewOptions`, `enableConsoleLogging`, `validateOption`, `validateOptions`, and `logZodIssues` as API functions. - Small corrections of the `_attachProcessExitListeners` (renamed from the `attachProcessExitListeners`). - Refactored logic for initial configuration, startup, and HTTP/HTTPS server management. - Optimized `startServer`, utilizing `updateOptions` for global option updates. -- The `startServer` now have its options parameters set as optional, using global option values if not provided. +- The `startServer` now have its options parameters defaulted to an empty object, using global option values if none are provided. - Optimized logic and statistics display in `health.js` router. - Optimized logic and corrected the url (from `version/change` to `version_change`) in the `versionChange.js` router. - Refactored `exportHandler` (to `requestExport`) by moving some logic to the `validaion` middleware and refactoring the rest. @@ -89,6 +93,7 @@ _Enhancements:_ - Renamed the `get` function to `getBrowser`, the `create` function to `createBrowser`, and the `close` function to `closeBrowser`. - Renamed `triggerExport` to `createChart`, optimizing the options passed and processed in the `highcharts.js` module. - Improved the module's overall logic, optimizing logging functions and the initialization process. +- Added the `logZodIssues` function for displaying correctly formatted validation errors. - Added `pathToLog` to the module's logging options to remember the full path to the log file. - Added the `enableConsoleLogging` function. - Removed `listen` function and `listeners` array from the module's logging options. @@ -118,12 +123,14 @@ _Enhancements:_ - Enhanced all files with improved JSDoc tags, descriptions, and comments, adding extra tags such as `@overview`, `@async`, and `@function`. - Corrected function descriptions, parameter types, return values, and documented errors. - Fixed all tests, samples, and scenario runners. +- Added unit tests for validating each option from every source (CLI, config, environment variables). - Created, renamed, or removed various tests, samples, and scenario runners. - Removed separate test runner scripts. - Made a minor correction in the `build` script. - Updated package versions. - Corrected the description of options prioritization order in the `Configuration` section. - Added explanations of overall option handling, management, and processing, along with descriptions for each export method (`Options Handling` section and subsections). +- Added a description of options validation in the `Options Validation` section. - Added, updated, corrected, or redefined descriptions and values of options in the following sections: `Default JSON Config`, `Environment Variables`, `Custom JSON Config`, `Command Line Arguments`, `HTTP Server POST Arguments`. - 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. diff --git a/README.md b/README.md index 2fa44897..a1df5dc0 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,10 @@ The `singleExport()`, `batchExport()`, and `startExport()` functions must be pro Essentially, all options can be configured through `.env`, the CLI, and prompts, with one exception: the `HIGHCHARTS_ADMIN_TOKEN`, which is only available as an environment variable. +## Options Validation + +By default, options validation is enabled, and it is recommended to keep it enabled to ensure that the provided options are correctly checked, validated, and parsed, allowing the exporting process to function without issues. However, it is possible to disable validation (by setting the `validation` option to **false**) if you are confident in the accuracy of the data you provide. Additionaly, when used as a Node.js module, each API function that updates global options with the provided data also offers the ability to validate the data. + ## Default JSON Config The JSON below represents the default configuration stored in the `./lib/schemas/config.js` file. If no `.env` file is found (more details on the file and environment variables below), these options will be used. The configuration is not recommended to be modified directly, as it can typically be managed through other sources. @@ -296,7 +300,8 @@ _Available default JSON config:_ "listenToProcessExits": true, "noLogo": false, "hardResetPage": false, - "browserShellMode": true + "browserShellMode": true, + "validation": true }, "debug": { "enable": false, @@ -428,6 +433,7 @@ _Available environment variables:_ - `OTHER_NO_LOGO`: Skip printing the logo on a startup. Will be replaced by a simple text (defaults to `false`). - `OTHER_HARD_RESET_PAGE`: Determines whether the page's content should be reset from scratch, including Highcharts scripts (defaults to `false`). - `OTHER_BROWSER_SHELL_MODE`: Decides whether to enable older but much more performant _shell_ mode for the browser (defaults to `true`). +- `OTHER_VALIDATION`: Decides whether or not to enable validation of options types (defaults to `true`). ### Debugging Config @@ -563,6 +569,7 @@ _Available CLI arguments:_ - `--noLogo`: Skip printing the logo on a startup. Will be replaced by a simple text (defaults to `false`). - `--hardResetPage`: Determines whether the page's content should be reset from scratch, including Highcharts scripts (defaults to `false`). - `--browserShellMode`: Decides whether to enable older but much more performant _shell_ mode for the browser (defaults to `true`). +- `--validation`: Decides whether or not to enable validation of options types (defaults to `true`). ### Debugging Config @@ -716,9 +723,9 @@ This package supports both CommonJS and ES modules. **highcharts-export-server module** -- `async function startServer(serverOptions)`: Starts an HTTP and/or HTTPS server based on the provided configuration. The `serverOptions` object contains server-related properties (refer to the `server` section in the `./lib/schemas/config.js` file for details). +- `async function startServer(serverOptions = {})`: Starts an HTTP and/or HTTPS server based on the provided configuration. The `serverOptions` object contains server-related properties (refer to the `server` section in the `./lib/schemas/config.js` file for details). - - `@param {Object} serverOptions` - The configuration object containing `server` options. This object may include a partial or complete set of the `server` options. If the options are partial, missing values will default to the current global configuration. + - `@param {Object} [serverOptions={}]` - The configuration object containing `server` options. This object may include a partial or complete set of the `server` options. If the options are partial, missing values will default to the current global configuration. The default value is an empty object. - `@returns {Promise}` A Promise that resolves when the server is either not enabled or no valid Express app is found, signaling the end of the function's execution. @@ -763,10 +770,11 @@ This package supports both CommonJS and ES modules. - `@returns {Object}` A copy of the global options object, or a reference to the global options object. -- `function updateOptions(newOptions, getCopy = false)`: Updates and returns the global options object or a copy of the global options object, based on the `getCopy` flag. +- `function updateOptions(newOptions, getCopy = false, strictCheck = true)`: Updates and returns the global options object or a copy of the global options object, based on the `getCopy` flag. The `newOptions` object can be strictly validated depending on the `strictCheck` flag. - `@param {Object} newOptions` - An object containing the new options to be merged into the global options. - `@param {boolean} [getCopy=false]` - Determines whether to merge the new options into a copy of the global options object (`true`) or directly into the global options object (`false`). The default value is `false`. + - `@param {boolean} [strictCheck=true]` - Determines if stricter validation should be applied. The default value is `true`. - `@returns {Object}` The updated options object, either the modified global options or a modified copy, based on the value of `getCopy`. @@ -776,6 +784,21 @@ This package supports both CommonJS and ES modules. - `@returns {Object}` A new object containing options structured according to the mapping defined in the `nestedProps` object or an empty object if the provided `oldOptions` is not a correct object. +- `function validateOption(name, configOption, strictCheck = true)`: Validates a specified option using the corresponding validator from the configuration object. Returns the original option if the validation is disabled globally. + + - `@param {string} name` - The name of the option to validate. + - `@param {any} configOption` - The value of the option to validate. + - `@param {boolean} [strictCheck=true]` - Determines if stricter validation should be applied. The default value is `true`. + + - `@returns {any}` The parsed and validated value of the option. + +- `function validateOptions(configOptions, strictCheck = true)`: Validates the provided configuration options for the exporting process. Returns the original option if the validation is disabled globally. + + - `@param {Object} configOptions` - The configuration options to be validated. + - `@param {boolean} [strictCheck=true]` - Determines if stricter validation should be applied. The default value is `true`. + + - `@returns {Object}` The parsed and validated configuration options object. + - `async function initExport(initOptions = {})`: Initializes the export process. Tasks such as configuring logging, checking the cache and sources, and initializing the resource pool occur during this stage. This function must be called before attempting to export charts or set up a server. @@ -831,6 +854,12 @@ This package supports both CommonJS and ES modules. - `@returns {void}` Exits the function execution if attempting to log at a level higher than allowed. +- `function logZodIssues(newLevel, issues, customMessage)`: Logs an error message related to Zod validation issues. Optionally, a custom message can be provided. + + - `@param {number} newLevel` - The log level. + - `@param {Error[]} issues` - An array of Zod validation issues. + - `@param {string} customMessage` - An optional custom message to be included in the log alongside the error. + - `function setLogLevel(level)`: Sets the log level to the specified value. Log levels are (`0` = no logging, `1` = error, `2` = warning, `3` = notice, `4` = verbose, or `5` = benchmark). - `@param {number} level` - The log level to be set. diff --git a/lib/chart.js b/lib/chart.js index e3665637..7c33c24f 100644 --- a/lib/chart.js +++ b/lib/chart.js @@ -21,7 +21,7 @@ See LICENSE file in root for details. import { readFileSync, writeFileSync } from 'fs'; -import { isAllowedConfig, updateOptions } from './config.js'; +import { isAllowedConfig, updateOptions, validateOption } from './config.js'; import { log, logWithStack } from './logger.js'; import { getPoolStats, killPool, postWork } from './pool.js'; import { sanitize } from './sanitize.js'; @@ -285,10 +285,10 @@ export async function startExport(imageOptions, endCallback) { // Check the file's extension if (exportOptions.infile.endsWith('.svg')) { // Set to the `svg` option - exportOptions.svg = fileContent; + exportOptions.svg = validateOption('svg', fileContent); } else if (exportOptions.infile.endsWith('.json')) { // Set to the `instr` option - exportOptions.instr = fileContent; + exportOptions.instr = validateOption('instr', fileContent); } else { throw new ExportError( '[chart] Incorrect value of the `infile` option.', @@ -716,6 +716,12 @@ function _handleCustomLogic(customLogicOptions) { customLogicOptions.allowFileResources, true ); + + // Validate option + customLogicOptions.resources = validateOption( + 'resources', + customLogicOptions.resources + ); } catch (error) { log(2, '[chart] The `resources` cannot be loaded.'); @@ -730,6 +736,12 @@ function _handleCustomLogic(customLogicOptions) { customLogicOptions.customCode, customLogicOptions.allowFileResources ); + + // Validate the option + customLogicOptions.customCode = validateOption( + 'customCode', + customLogicOptions.customCode + ); } catch (error) { logWithStack(2, error, '[chart] The `customCode` cannot be loaded.'); @@ -745,6 +757,12 @@ function _handleCustomLogic(customLogicOptions) { customLogicOptions.allowFileResources, true ); + + // Validate the option + customLogicOptions.callback = validateOption( + 'callback', + customLogicOptions.callback + ); } catch (error) { logWithStack(2, error, '[chart] The `callback` cannot be loaded.'); @@ -956,6 +974,12 @@ function _handleGlobalAndTheme(exportOptions, customLogicOptions) { allowCodeExecution ); } + + // Validate the option + exportOptions[optionsName] = validateOption( + optionsName, + exportOptions[optionsName] + ); } } catch (error) { logWithStack( diff --git a/lib/config.js b/lib/config.js index a7c1149a..b48b0017 100644 --- a/lib/config.js +++ b/lib/config.js @@ -22,12 +22,19 @@ See LICENSE file in root for details. import { readFileSync } from 'fs'; -import { envs } from './envs.js'; -import { log, logWithStack } from './logger.js'; +import { log, logWithStack, logZodIssues } from './logger.js'; import { deepCopy, getAbsolutePath, isObject } from './utils.js'; +import { + envs, + looseValidate, + strictValidate, + validators +} from './validation.js'; import defaultConfig from './schemas/config.js'; +import ExportError from './errors/ExportError.js'; + // Sets the global options with initial values from the default config const globalOptions = _initOptions(defaultConfig); @@ -57,7 +64,8 @@ export function getOptions(getCopy = true) { /** * Updates and returns the global options object or a copy of the global options - * object, based on the `getCopy` flag. + * object, based on the `getCopy` flag. The `newOptions` object can be + * strictly validated depending on the `strictCheck` flag. * * @function updateOptions * @@ -66,13 +74,20 @@ export function getOptions(getCopy = true) { * @param {boolean} [getCopy=false] - Determines whether to merge the new * options into a copy of the global options object (`true`) or directly into * the global options object (`false`). The default value is `false`. + * @param {boolean} [strictCheck=true] - Determines if stricter validation + * should be applied. The default value is `true`. * * @returns {Object} The updated options object, either the modified global * options or a modified copy, based on the value of `getCopy`. */ -export function updateOptions(newOptions, getCopy = false) { +export function updateOptions(newOptions, getCopy = false, strictCheck = true) { // Merge new options to the global options or its copy and return the result - return _mergeOptions(getOptions(getCopy), newOptions); + return _mergeOptions( + // First, get the options + getOptions(getCopy), + // Next, validate the new options + validateOptions(newOptions, strictCheck) + ); } /** @@ -97,17 +112,25 @@ export function updateOptions(newOptions, getCopy = false) { export function setCliOptions(cliArgs) { // Only for the CLI usage if (cliArgs && Array.isArray(cliArgs) && cliArgs.length) { - // Get options from the custom JSON loaded via the `--loadConfig` - const configOptions = _loadConfigFile(cliArgs); + try { + // Get options from the custom JSON loaded via the `--loadConfig` + const configOptions = _loadConfigFile(cliArgs); - // Update global options with the values from the `configOptions` object - updateOptions(configOptions); + // Update global options with validated values from the `configOptions` + updateOptions(configOptions); + } catch (error) { + log(2, '[config] No options added from the `--loadConfig` option.'); + } - // Get options from the CLI - const cliOptions = _pairArgumentValue(cliArgs); + try { + // Get options from the CLI + const cliOptions = _pairArgumentValue(cliArgs); - // Update global options with the values from the `cliOptions` object - updateOptions(cliOptions); + // Update global options with validated values from the `cliOptions` + updateOptions(cliOptions, false, false); + } catch (error) { + log(2, '[config] No options added from the CLI arguments.'); + } } // Return reference to the global options @@ -163,6 +186,77 @@ export function mapToNewOptions(oldOptions) { return newOptions; } +/** + * Validates a specified option using the corresponding validator from the + * configuration object. Returns the original option if the validation + * is disabled globally. + * + * @function validateOption + * + * @param {string} name - The name of the option to validate. + * @param {any} configOption - The value of the option to validate. + * @param {boolean} [strictCheck=true] - Determines if stricter validation + * should be applied. The default value is `true`. + * + * @returns {any} The parsed and validated value of the option. + */ +export function validateOption(name, configOption, strictCheck = true) { + // Return the original option if the validation is disabled + if (!getOptions().other.validation) { + return configOption; + } + + try { + // Return validated option + return validators[name](strictCheck).parse(configOption); + } catch (error) { + // Log Zod issues + logZodIssues( + 1, + error.issues, + `[validation] The ${name} option validation error` + ); + + // Throw validation error + throw new ExportError( + `[validation] The ${name} option validation error`, + 400 + ); + } +} + +/** + * Validates the provided configuration options for the exporting process. + * Returns the original option if the validation is disabled globally. + * + * @function validateOptions + * + * @param {Object} configOptions - The configuration options to be validated. + * @param {boolean} [strictCheck=true] - Determines if stricter validation + * should be applied. The default value is `true`. + * + * @returns {Object} The parsed and validated configuration options object. + */ +export function validateOptions(configOptions, strictCheck = true) { + // Return the original config if the validation is disabled + if (!getOptions().other.validation) { + return configOptions; + } + + try { + // Return validated options + return strictCheck + ? strictValidate(configOptions) + : looseValidate(configOptions); + } catch (error) { + // Log Zod issues + logZodIssues(1, error.issues, '[validation] Options validation error'); + + // Throw validation error + throw new ExportError('[validation] Options validation error', 400); + } +} + /** * Validates, parses, and checks if the provided config is allowed set * of options. @@ -542,5 +636,7 @@ export default { updateOptions, setCliOptions, mapToNewOptions, + validateOption, + validateOptions, isAllowedConfig }; diff --git a/lib/envs.js b/lib/envs.js deleted file mode 100644 index 3393aa2a..00000000 --- a/lib/envs.js +++ /dev/null @@ -1,263 +0,0 @@ -/******************************************************************************* - -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 This file is responsible for parsing the environment variables - * with the 'zod' library. The parsed environment variables are then exported - * to be used in the application as `envs`. We should not use the `process.env` - * directly in the application as these would not be parsed properly. - * - * The environment variables are parsed and validated only once when - * the application starts. We should write a custom validator or a transformer - * for each of the options. - */ - -import dotenv from 'dotenv'; -import { z } from 'zod'; - -import defaultConfig from './schemas/config.js'; - -// Load .env into environment variables -dotenv.config(); - -// Object with custom validators and transformers, to avoid repetition -// in the Config object -const v = { - // Splits string value into elements in an array, trims every element, checks - // if an array is correct, if it is empty, and if it is, returns undefined - array: (filterArray) => - z - .string() - .transform((value) => - value - .split(',') - .map((value) => value.trim()) - .filter((value) => filterArray.includes(value)) - ) - .transform((value) => (value.length ? value : undefined)), - - // Allows only true, false and correctly parse the value to boolean - // or no value in which case the returned value will be undefined - boolean: () => - z - .enum(['true', 'false', '']) - .transform((value) => (value !== '' ? value === 'true' : undefined)), - - // Allows passed values or no value in which case the returned value will - // be undefined - enum: (values) => - z - .enum([...values, '']) - .transform((value) => (value !== '' ? value : undefined)), - - // Trims the string value and checks if it is empty or contains stringified - // values such as false, undefined, null, NaN, if it does, returns undefined - string: () => - z - .string() - .trim() - .refine( - (value) => - !['false', 'undefined', 'null', 'NaN'].includes(value) || - value === '', - (value) => ({ - message: `The string contains forbidden values, received '${value}'` - }) - ) - .transform((value) => (value !== '' ? value : undefined)), - - // Allows positive numbers or no value in which case the returned value will - // be undefined - positiveNum: () => - z - .string() - .trim() - .refine( - (value) => - value === '' || (!isNaN(parseFloat(value)) && parseFloat(value) > 0), - (value) => ({ - message: `The value must be numeric and positive, received '${value}'` - }) - ) - .transform((value) => (value !== '' ? parseFloat(value) : undefined)), - - // Allows non-negative numbers or no value in which case the returned value - // will be undefined - nonNegativeNum: () => - z - .string() - .trim() - .refine( - (value) => - value === '' || (!isNaN(parseFloat(value)) && parseFloat(value) >= 0), - (value) => ({ - message: `The value must be numeric and non-negative, received '${value}'` - }) - ) - .transform((value) => (value !== '' ? parseFloat(value) : undefined)) -}; - -export const Config = z.object({ - // puppeteer - PUPPETEER_ARGS: v.string(), - - // highcharts - HIGHCHARTS_VERSION: z - .string() - .trim() - .refine( - (value) => /^(latest|\d+(\.\d+){0,2})$/.test(value) || value === '', - (value) => ({ - message: `HIGHCHARTS_VERSION must be 'latest', a major version, or in the form XX.YY.ZZ, received '${value}'` - }) - ) - .transform((value) => (value !== '' ? value : undefined)), - HIGHCHARTS_CDN_URL: z - .string() - .trim() - .refine( - (value) => - value.startsWith('https://') || - value.startsWith('http://') || - value === '', - (value) => ({ - message: `Invalid value for HIGHCHARTS_CDN_URL. It should start with http:// or https://, received '${value}'` - }) - ) - .transform((value) => (value !== '' ? value : undefined)), - HIGHCHARTS_FORCE_FETCH: v.boolean(), - HIGHCHARTS_CACHE_PATH: v.string(), - HIGHCHARTS_ADMIN_TOKEN: v.string(), - HIGHCHARTS_CORE_SCRIPTS: v.array(defaultConfig.highcharts.coreScripts.value), - HIGHCHARTS_MODULE_SCRIPTS: v.array( - defaultConfig.highcharts.moduleScripts.value - ), - HIGHCHARTS_INDICATOR_SCRIPTS: v.array( - defaultConfig.highcharts.indicatorScripts.value - ), - HIGHCHARTS_CUSTOM_SCRIPTS: v.array( - defaultConfig.highcharts.customScripts.value - ), - - // export - EXPORT_INFILE: v.string(), - EXPORT_INSTR: v.string(), - EXPORT_OPTIONS: v.string(), - EXPORT_SVG: v.string(), - EXPORT_BATCH: v.string(), - EXPORT_OUTFILE: v.string(), - EXPORT_TYPE: v.enum(['jpeg', 'png', 'pdf', 'svg']), - EXPORT_CONSTR: v.enum(['chart', 'stockChart', 'mapChart', 'ganttChart']), - EXPORT_B64: v.boolean(), - EXPORT_NO_DOWNLOAD: v.boolean(), - EXPORT_HEIGHT: v.positiveNum(), - EXPORT_WIDTH: v.positiveNum(), - EXPORT_SCALE: v.positiveNum(), - EXPORT_DEFAULT_HEIGHT: v.positiveNum(), - EXPORT_DEFAULT_WIDTH: v.positiveNum(), - EXPORT_DEFAULT_SCALE: v.positiveNum(), - EXPORT_GLOBAL_OPTIONS: v.string(), - EXPORT_THEME_OPTIONS: v.string(), - EXPORT_RASTERIZATION_TIMEOUT: v.nonNegativeNum(), - - // custom - CUSTOM_LOGIC_ALLOW_CODE_EXECUTION: v.boolean(), - CUSTOM_LOGIC_ALLOW_FILE_RESOURCES: v.boolean(), - CUSTOM_LOGIC_CUSTOM_CODE: v.string(), - CUSTOM_LOGIC_CALLBACK: v.string(), - CUSTOM_LOGIC_RESOURCES: v.string(), - CUSTOM_LOGIC_LOAD_CONFIG: v.string(), - CUSTOM_LOGIC_CREATE_CONFIG: v.string(), - - // server - SERVER_ENABLE: v.boolean(), - SERVER_HOST: v.string(), - SERVER_PORT: v.positiveNum(), - SERVER_UPLOAD_LIMIT: v.positiveNum(), - SERVER_BENCHMARKING: v.boolean(), - - // server proxy - SERVER_PROXY_HOST: v.string(), - SERVER_PROXY_PORT: v.positiveNum(), - SERVER_PROXY_TIMEOUT: v.nonNegativeNum(), - - // server rate limiting - SERVER_RATE_LIMITING_ENABLE: v.boolean(), - SERVER_RATE_LIMITING_MAX_REQUESTS: v.nonNegativeNum(), - SERVER_RATE_LIMITING_WINDOW: v.nonNegativeNum(), - SERVER_RATE_LIMITING_DELAY: v.nonNegativeNum(), - SERVER_RATE_LIMITING_TRUST_PROXY: v.boolean(), - SERVER_RATE_LIMITING_SKIP_KEY: v.string(), - SERVER_RATE_LIMITING_SKIP_TOKEN: v.string(), - - // server ssl - SERVER_SSL_ENABLE: v.boolean(), - SERVER_SSL_FORCE: v.boolean(), - SERVER_SSL_PORT: v.positiveNum(), - SERVER_SSL_CERT_PATH: v.string(), - - // pool - POOL_MIN_WORKERS: v.nonNegativeNum(), - POOL_MAX_WORKERS: v.nonNegativeNum(), - POOL_WORK_LIMIT: v.positiveNum(), - POOL_ACQUIRE_TIMEOUT: v.nonNegativeNum(), - POOL_CREATE_TIMEOUT: v.nonNegativeNum(), - POOL_DESTROY_TIMEOUT: v.nonNegativeNum(), - POOL_IDLE_TIMEOUT: v.nonNegativeNum(), - POOL_CREATE_RETRY_INTERVAL: v.nonNegativeNum(), - POOL_REAPER_INTERVAL: v.nonNegativeNum(), - POOL_BENCHMARKING: v.boolean(), - - // logger - LOGGING_LEVEL: z - .string() - .trim() - .refine( - (value) => - value === '' || - (!isNaN(parseFloat(value)) && - parseFloat(value) >= 0 && - parseFloat(value) <= 5), - (value) => ({ - message: `Invalid value for LOGGING_LEVEL. We only accept values from 0 to 5 as logging levels, received '${value}'` - }) - ) - .transform((value) => (value !== '' ? parseFloat(value) : undefined)), - LOGGING_FILE: v.string(), - LOGGING_DEST: v.string(), - LOGGING_TO_CONSOLE: v.boolean(), - LOGGING_TO_FILE: v.boolean(), - - // ui - UI_ENABLE: v.boolean(), - UI_ROUTE: v.string(), - - // other - OTHER_NODE_ENV: v.enum(['development', 'production', 'test']), - OTHER_LISTEN_TO_PROCESS_EXITS: v.boolean(), - OTHER_NO_LOGO: v.boolean(), - OTHER_HARD_RESET_PAGE: v.boolean(), - OTHER_BROWSER_SHELL_MODE: v.boolean(), - - // debugger - DEBUG_ENABLE: v.boolean(), - DEBUG_HEADLESS: v.boolean(), - DEBUG_DEVTOOLS: v.boolean(), - DEBUG_LISTEN_TO_CONSOLE: v.boolean(), - DEBUG_DUMPIO: v.boolean(), - DEBUG_SLOW_MO: v.nonNegativeNum(), - DEBUG_DEBUGGING_PORT: v.positiveNum() -}); - -export const envs = Config.partial().parse(process.env); diff --git a/lib/index.js b/lib/index.js index 07f7efa2..b011b6ae 100644 --- a/lib/index.js +++ b/lib/index.js @@ -30,10 +30,17 @@ import { startExport, setAllowCodeExecution } from './chart.js'; -import { getOptions, updateOptions, mapToNewOptions } from './config.js'; +import { + getOptions, + updateOptions, + mapToNewOptions, + validateOption, + validateOptions +} from './config.js'; import { log, logWithStack, + logZodIssues, initLogging, enableConsoleLogging, enableFileLogging, @@ -55,12 +62,13 @@ import server from './server/server.js'; * @async * @function initExport * - * @param {Object} initOptions - The `initOptions` object, which may + * @param {Object} [initOptions={}] - The `initOptions` object, which may * be a partial or complete set of options. If the options are partial, missing - * values will default to the current global configuration. + * values will default to the current global configuration. The default value + * is an empty object. */ -export async function initExport(initOptions) { - // Init and update the instance options object +export async function initExport(initOptions = {}) { + // Init, validate and update the options object const options = updateOptions(initOptions); // Set the `allowCodeExecution` per export module scope @@ -130,6 +138,10 @@ export default { updateOptions, mapToNewOptions, + // Validation + validateOption, + validateOptions, + // Exporting initExport, singleExport, @@ -143,6 +155,7 @@ export default { // Logs log, logWithStack, + logZodIssues, setLogLevel: function (level) { // Update the instance options object const options = updateOptions({ diff --git a/lib/logger.js b/lib/logger.js index 5972777c..19a0a15a 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -159,6 +159,28 @@ export function logWithStack(newLevel, error, customMessage) { } } +/** + * Logs an error message related to Zod validation issues. Optionally, a custom + * message can be provided. + * + * @function logZodIssues + * + * @param {number} newLevel - The log level. + * @param {Error[]} issues - An array of Zod validation issues. + * @param {string} customMessage - An optional custom message to be included + * in the log alongside the error. + */ +export function logZodIssues(newLevel, issues, customMessage) { + logWithStack( + newLevel, + null, + [ + `${customMessage || '[validation] Validation error'} - the following Zod issues occured:`, + ...(issues || []).map((issue) => `- ${issue.message}`) + ].join('\n') + ); +} + /** * Initializes logging with the specified logging configuration. * @@ -279,6 +301,7 @@ function _logToFile(texts, prefix) { export default { log, logWithStack, + logZodIssues, initLogging, setLogLevel, enableConsoleLogging, diff --git a/lib/pool.js b/lib/pool.js index d3847748..2c8fd9e7 100644 --- a/lib/pool.js +++ b/lib/pool.js @@ -23,7 +23,7 @@ See LICENSE file in root for details. import { Pool } from 'tarn'; import { v4 as uuid } from 'uuid'; -import { createBrowser, closeBrowser, newPage, clearPage } from './browser.js'; +import { clearPage, createBrowser, closeBrowser, newPage } from './browser.js'; import { puppeteerExport } from './export.js'; import { log, logWithStack } from './logger.js'; import { getNewDateTime, measureTime } from './utils.js'; diff --git a/lib/prompt.js b/lib/prompt.js index 3d018b8b..528c17f9 100644 --- a/lib/prompt.js +++ b/lib/prompt.js @@ -21,7 +21,7 @@ import { existsSync, readFileSync, writeFileSync } from 'fs'; import prompts from 'prompts'; -import { isAllowedConfig } from './config.js'; +import { isAllowedConfig, validateOptions } from './config.js'; import { log, logWithStack } from './logger.js'; import { getAbsolutePath } from './utils.js'; @@ -163,6 +163,9 @@ export async function manualConfig(fileName, allowCodeExecution) { // If all questions have been answered, save the updated config if (++questionsCounter === allQuestions.length) { try { + // Validate the prompt result + configFile = validateOptions(configFile); + // Save the prompt result writeFileSync( getAbsolutePath(fileName), diff --git a/lib/schemas/config.js b/lib/schemas/config.js index c8d1e677..ee12aec4 100644 --- a/lib/schemas/config.js +++ b/lib/schemas/config.js @@ -917,6 +917,15 @@ const defaultConfig = { promptOptions: { type: 'toggle' } + }, + validation: { + value: true, + types: ['boolean'], + envLink: 'OTHER_VALIDATION', + description: 'Whether or not to enable validation of options types', + promptOptions: { + type: 'toggle' + } } }, debug: { diff --git a/lib/server/middlewares/validation.js b/lib/server/middlewares/validation.js index 96d6b51d..2ea2ca32 100644 --- a/lib/server/middlewares/validation.js +++ b/lib/server/middlewares/validation.js @@ -110,7 +110,7 @@ function requestBodyMiddleware(request, response, next) { ); } - // Get the allowCodeExecution option for the server + // Get the `allowCodeExecution` option for the server const allowCodeExecution = getAllowCodeExecution(); // Find a correct chart options @@ -146,7 +146,7 @@ function requestBodyMiddleware(request, response, next) { ); } - // Get the request options and store parsed structure in the request + // Get and pre-validate the options and store them in the request request.validatedOptions = { // Set the created ID as a `requestId` property in the options requestId, diff --git a/lib/server/routes/health.js b/lib/server/routes/health.js index dd9345fa..78839c06 100644 --- a/lib/server/routes/health.js +++ b/lib/server/routes/health.js @@ -22,7 +22,7 @@ import { join } from 'path'; import { getHcVersion } from '../../cache.js'; import { log } from '../../logger.js'; -import { getPoolStats, getPoolInfoJSON } from '../../pool.js'; +import { getPoolInfoJSON, getPoolStats } from '../../pool.js'; import { addTimer } from '../../timer.js'; import { __dirname, getNewDateTime } from '../../utils.js'; diff --git a/lib/server/routes/versionChange.js b/lib/server/routes/versionChange.js index 976a1ccb..0a712303 100644 --- a/lib/server/routes/versionChange.js +++ b/lib/server/routes/versionChange.js @@ -18,8 +18,8 @@ See LICENSE file in root for details. */ import { getHcVersion, updateHcVersion } from '../../cache.js'; -import { envs } from '../../envs.js'; import { log } from '../../logger.js'; +import { envs } from '../../validation.js'; import ExportError from '../../errors/ExportError.js'; diff --git a/lib/server/server.js b/lib/server/server.js index d1120384..a68045ca 100644 --- a/lib/server/server.js +++ b/lib/server/server.js @@ -59,10 +59,11 @@ const app = express(); * @async * @function startServer * - * @param {Object} serverOptions - The configuration object containing `server` - * options. This object may include a partial or complete set of the `server` - * options. If the options are partial, missing values will default - * to the current global configuration. + * @param {Object} [serverOptions={}] - The configuration object containing + * `server` options. This object may include a partial or complete set + * of the `server` options. If the options are partial, missing values will + * default to the current global configuration. The default value is an empty + * object. * * @returns {Promise} A Promise that resolves when the server is either * not enabled or no valid Express app is found, signaling the end of the @@ -71,7 +72,7 @@ const app = express(); * @throws {ExportError} Throws an `ExportError` if the server cannot * be configured and started. */ -export async function startServer(serverOptions) { +export async function startServer(serverOptions = {}) { try { // Update the instance options object const options = updateOptions({ diff --git a/lib/validation.js b/lib/validation.js new file mode 100644 index 00000000..b7d35b62 --- /dev/null +++ b/lib/validation.js @@ -0,0 +1,2820 @@ +/******************************************************************************* + +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 This file handles parsing and validating options from multiple + * sources (the config file, custom JSON, environment variables, CLI arguments, + * and request payload) using the 'zod' library. + * + * Environment variables are parsed and validated only once at application + * startup, and the validated results are exported as `envs` for use throughout + * the application. + * + * Options from other sources, however, are parsed and validated on demand, + * each time an export is attempted. + */ + +import dotenv from 'dotenv'; +import { z } from 'zod'; + +import defaultConfig from './schemas/config.js'; + +// Load the .env into environment variables +dotenv.config(); + +// Get scripts names of each category from the default config +const { coreScripts, moduleScripts, indicatorScripts } = + defaultConfig.highcharts; + +// Sets the custom error map globally +z.setErrorMap(_customErrorMap); + +/** + * Object containing custom general validators and parsers to avoid repetition + * in schema objects. All validators apply to values from various sources, + * including the default config file, a custom JSON file loaded with the option + * called `loadConfig`, the .env file, CLI arguments, and the request payload. + * The `strictCheck` flag enables stricter validation and parsing rules. This + * flag is set to false for values that come from the .env file or CLI arguments + * because they are provided as strings and need to be parsed accordingly first. + */ +const v = { + /** + * The `boolean` validator that returns a Zod schema with an optional stricter + * check based on the `strictCheck` parameter. + * + * The validation schema ensures that: + * + * - When `strictCheck` is true, the schema will accept values are true + * and false and the schema will validate against the default boolean + * validator. + * + * - When `strictCheck` is false, the schema will accept values are true, + * false, null, 'true', '1', 'false', '0', 'undefined', 'null', and ''. + * The strings 'undefined', 'null', and '' will be transformed to null, + * the string 'true' will be transformed to the boolean value true, + * and 'false' will be transformed to the boolean value false. + * + * @function boolean + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating boolean values. + */ + boolean(strictCheck) { + return strictCheck + ? z.boolean() + : z + .union([ + z + .enum(['true', '1', 'false', '0', 'undefined', 'null', '']) + .transform((value) => + !['undefined', 'null', ''].includes(value) + ? value === 'true' || value === '1' + : null + ), + z.boolean() + ]) + .nullable(); + }, + + /** + * The `string` validator that returns a Zod schema with an optional stricter + * check based on the `strictCheck` parameter. + * + * The validation schema ensures that: + * + * - When `strictCheck` is true, the schema will accept trimmed strings except + * the forbidden values: 'false', 'undefined', 'null', and ''. + * + * - When `strictCheck` is false, the schema will accept trimmed strings + * and null. The forbidden values: 'false', 'undefined', 'null', and '' will + * be transformed to null. + * + * @function string + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating string values. + */ + string(strictCheck) { + return strictCheck + ? z + .string() + .trim() + .refine( + (value) => !['false', 'undefined', 'null', ''].includes(value), + { + params: { + errorMessage: 'The string contains a forbidden value' + } + } + ) + : z + .string() + .trim() + .transform((value) => + !['false', 'undefined', 'null', ''].includes(value) ? value : null + ) + .nullable(); + }, + + /** + * The `enum` validator that returns a Zod schema with an optional stricter + * check based on the `strictCheck` parameter. + * + * The schema will validate against the provided `values` array. + * + * The validation schema ensures that: + * + * - When `strictCheck` is true, the schema will validate against the `values` + * array with the default enum validator. + * + * - When `strictCheck` is false, the schema will accept also null, + * 'undefined', 'null', and '', which will be transformed to null. + * + * @function enum + * + * @param {Array.} values - An array of valid string values + * for the enum. + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating enum values. + */ + enum(values, strictCheck) { + return strictCheck + ? z.enum([...values]) + : z + .enum([...values, 'undefined', 'null', '']) + .transform((value) => + !['undefined', 'null', ''].includes(value) ? value : null + ) + .nullable(); + }, + + /** + * The `stringArray` validator that returns a Zod schema with an optional + * stricter check based on the `strictCheck` parameter. + * + * The validation schema ensures that: + * + * - When `strictCheck` is true, the schema will accept an array of trimmed + * string values filtered by the logic provided through the `filterCallback`. + * + * - When `strictCheck` is false, the schema will accept null and trimmed + * string values which will be splitted into an array of strings and filtered + * from the '[' and ']' characters and by the logic provided through + * the `filterCallback`. If the array is empty, it will be transformed + * to null. + * + * @function stringArray + * + * @param {function} filterCallback - The filter callback. + * @param {string} separator - The separator for spliting a string. + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating array of string + * values. + */ + stringArray(filterCallback, separator, strictCheck) { + const arraySchema = z.string().trim().array(); + const stringSchema = z + .string() + .trim() + .transform((value) => { + if (value.startsWith('[')) { + value = value.slice(1); + } + if (value.endsWith(']')) { + value = value.slice(0, -1); + } + return value.split(separator); + }); + + const transformCallback = (value) => + value.map((value) => value.trim()).filter(filterCallback); + + return strictCheck + ? arraySchema.transform(transformCallback) + : z + .union([stringSchema, arraySchema]) + .transform(transformCallback) + .transform((value) => (value.length ? value : null)) + .nullable(); + }, + + /** + * The `positiveNum` validator that returns a Zod schema with an optional + * stricter check based on the `strictCheck` parameter. + * + * The validation schema ensures that: + * + * - When `strictCheck` is true, the schema will accept positive number values + * and validate against the default positive number validator. + * + * - When `strictCheck` is false, the schema will accept positive number + * values, null, and trimmed string values that can either be 'undefined', + * 'null', '', or represent a positive number. It will transform the string + * to a positive number, or to null if it is 'undefined', 'null', or ''. + * + * @function positiveNum + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating positive number + * values. + */ + positiveNum(strictCheck) { + return strictCheck + ? z.number().positive() + : z + .union([ + z + .string() + .trim() + .refine( + (value) => + (!isNaN(Number(value)) && Number(value) > 0) || + ['undefined', 'null', ''].includes(value), + { + params: { + errorMessage: 'The value must be numeric and positive' + } + } + ) + .transform((value) => + !['undefined', 'null', ''].includes(value) + ? Number(value) + : null + ), + z.number().positive() + ]) + .nullable(); + }, + + /** + * The `nonNegativeNum` validator that returns a Zod schema with an optional + * stricter check based on the `strictCheck` parameter. + * + * The validation schema ensures that: + * + * - When `strictCheck` is true, the schema will accept non-negative number + * values and validate against the default non-negative number validator. + * + * - When `strictCheck` is false, the schema will accept non-negative number + * values, null, and trimmed string values that can either be 'undefined', + * 'null', '', or represent a non-negative number. It will transform + * the string to a non-negative number, or to null if it is 'undefined', + * 'null', or ''. + * + * @function nonNegativeNum + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating non-negative + * number values. + */ + nonNegativeNum(strictCheck) { + return strictCheck + ? z.number().nonnegative() + : z + .union([ + z + .string() + .trim() + .refine( + (value) => + (!isNaN(Number(value)) && Number(value) >= 0) || + ['undefined', 'null', ''].includes(value), + { + params: { + errorMessage: 'The value must be numeric and non-negative' + } + } + ) + .transform((value) => + !['undefined', 'null', ''].includes(value) + ? Number(value) + : null + ), + z.number().nonnegative() + ]) + .nullable(); + }, + + /** + * The `startsWith` validator that returns a Zod schema with an optional + * stricter check based on the `strictCheck` parameter. + * + * The schema will validate against the provided `prefixes` array to check + * whether a string value starts with any of the values provided + * in the `prefixes` array. + * + * The validation schema ensures that: + * + * - When `strictCheck` is true, the schema will accept trimmed string values + * that start with values from the prefixes array. + * + * - When `strictCheck` is false, the schema will accept trimmed string values + * that start with values from the prefixes array, null, 'undefined', 'null', + * and '' where the schema will transform them to null. + * + * @function startsWith + * + * @param {Array.} prefixes - An array of prefixes to validate + * the string against. + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating strings that + * starts with values. + */ + startsWith(prefixes, strictCheck) { + return strictCheck + ? z + .string() + .trim() + .refine( + (value) => prefixes.some((prefix) => value.startsWith(prefix)), + { + params: { + errorMessage: `The value must be a string that starts with ${prefixes.join(', ')}` + } + } + ) + : z + .string() + .trim() + .refine( + (value) => + prefixes.some((prefix) => value.startsWith(prefix)) || + ['undefined', 'null', ''].includes(value), + { + params: { + errorMessage: `The value must be a string that starts with ${prefixes.join(', ')}` + } + } + ) + .transform((value) => + !['undefined', 'null', ''].includes(value) ? value : null + ) + .nullable(); + }, + + /** + * The `chartConfig` validator that returns a Zod schema. + * + * The validation schema ensures that the schema will accept object values + * or trimmed string values that contain ' + (value.startsWith('{') && value.endsWith('}')) || + ['undefined', 'null', ''].includes(value), + { + params: { + errorMessage: + "The value must be a string that starts with '{' and ends with '}'" + } + } + ) + .transform((value) => + !['undefined', 'null', ''].includes(value) ? value : null + ), + z.object({}).passthrough() + ]) + .nullable(); + }, + + /** + * The `additionalOptions` validator that returns a Zod schema. + * + * The validation schema ensures that the schema will accept object values + * or trimmed string values that end with '.json' and are at least one + * character long excluding the extension, start with the '{' and end + * with the '}', and null. The 'undefined', 'null', and '' values will + * be transformed to null. + * + * @function additionalOptions + * + * @returns {z.ZodSchema} A Zod schema object for validating additional chart + * options value. + */ + additionalOptions() { + return z + .union([ + z + .string() + .trim() + .refine( + (value) => + (value.length >= 6 && value.endsWith('.json')) || + (value.startsWith('{') && value.endsWith('}')) || + ['undefined', 'null', ''].includes(value), + { + params: { + errorMessage: + "The value must be a string that ends with '.json' or starts with '{' and ends with '}'" + } + } + ) + .transform((value) => + !['undefined', 'null', ''].includes(value) ? value : null + ), + z.object({}).passthrough() + ]) + .nullable(); + } +}; + +/** + * Object containing custom config validators and parsers to avoid repetition + * in schema objects. All validators apply to values from various sources, + * including the default config file, a custom JSON file loaded with the option + * called `loadConfig`, the .env file, CLI arguments, and the request payload. + * The `strictCheck` flag enables stricter validation and parsing rules. This + * flag is set to false for values that come from the .env file or CLI arguments + * because they are provided as strings and need to be parsed accordingly first. + */ +export const validators = { + /** + * The `args` 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 `stringArray` validator. + * + * @function args + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `args` + * option. + */ + args(strictCheck) { + return v.stringArray( + (value) => !['false', 'undefined', 'null', ''].includes(value), + ';', + strictCheck + ); + }, + + /** + * The `version` validator that returns a Zod schema with an optional stricter + * check based on the `strictCheck` parameter. + * + * The validation schema ensures that: + * + * - When `strictCheck` is true, the schema will accept trimmed string values + * that are a RegExp-based that allows to be 'latest', or in the format XX, + * XX.YY, or XX.YY.ZZ, where XX, YY, and ZZ are numeric for the Highcharts + * version option. + * + * - When `strictCheck` is false, the schema will accept also null, + * 'undefined', 'null', or '' and in all cases the schema will transform them + * to null. + * + * @function version + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `version` + * option. + */ + version(strictCheck) { + return strictCheck + ? z + .string() + .trim() + .refine((value) => /^(latest|\d{1,2}(\.\d{1,2}){0,2})$/.test(value), { + params: { + errorMessage: + "The value must be 'latest', a major version, or in the form XX.YY.ZZ" + } + }) + : z + .string() + .trim() + .refine( + (value) => + /^(latest|\d{1,2}(\.\d{1,2}){0,2})$/.test(value) || + ['undefined', 'null', ''].includes(value), + { + params: { + errorMessage: + "The value must be 'latest', a major version, or in the form XX.YY.ZZ" + } + } + ) + .transform((value) => + !['undefined', 'null', ''].includes(value) ? value : null + ) + .nullable(); + }, + + /** + * The `cdnUrl` 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 cdnUrl + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `cdnUrl` + * option. + */ + cdnUrl(strictCheck) { + return v.startsWith(['http://', 'https://'], strictCheck); + }, + + /** + * The `forceFetch` 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 forceFetch + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `forceFetch` + * option. + */ + forceFetch(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `cachePath` 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 cachePath + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `cachePath` + * option. + */ + cachePath(strictCheck) { + return v.string(strictCheck); + }, + + /** + * The `adminToken` 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 adminToken + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `adminToken` + * option. + */ + adminToken(strictCheck) { + return v.string(strictCheck); + }, + + /** + * The `coreScripts` 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 `stringArray` validator. + * + * @function coreScripts + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `coreScripts` + * option. + */ + coreScripts(strictCheck) { + return v.stringArray( + (value) => coreScripts.value.includes(value), + ',', + strictCheck + ); + }, + + /** + * The `moduleScripts` 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 `stringArray` validator. + * + * @function moduleScripts + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `moduleScripts` option. + */ + moduleScripts(strictCheck) { + return v.stringArray( + (value) => moduleScripts.value.includes(value), + ',', + strictCheck + ); + }, + + /** + * The `indicatorScripts` 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 `stringArray` validator. + * + * @function indicatorScripts + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `indicatorScripts` option. + */ + indicatorScripts(strictCheck) { + return v.stringArray( + (value) => indicatorScripts.value.includes(value), + ',', + strictCheck + ); + }, + + /** + * The `customScripts` 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 `stringArray` validator. + * + * @function customScripts + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `customScripts` option. + */ + customScripts(strictCheck) { + return v.stringArray( + (value) => value.startsWith('https://') || value.startsWith('http://'), + ',', + strictCheck + ); + }, + + /** + * The `infile` validator that returns a Zod schema with an optional stricter + * check based on the `strictCheck` parameter. + * + * The validation schema ensures that: + * + * - When `strictCheck` is true, the schema will accept trimmed string values + * that end with '.json' or '.svg', are at least one character long excluding + * the extension, or null. + * + * - When `strictCheck` is false, the schema will accept trimmed string values + * that end with '.json' or '.svg', are at least one character long excluding + * the extension and will be null if the provided value is null, 'undefined', + * 'null', or ''. + * + * @function infile + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `infile` + * option. + */ + infile(strictCheck) { + return strictCheck + ? z + .string() + .trim() + .refine( + (value) => + (value.length >= 6 && value.endsWith('.json')) || + (value.length >= 5 && value.endsWith('.svg')), + { + params: { + errorMessage: + 'The value must be a string that ends with .json or .svg' + } + } + ) + .nullable() + : z + .string() + .trim() + .refine( + (value) => + (value.length >= 6 && value.endsWith('.json')) || + (value.length >= 5 && value.endsWith('.svg')) || + ['undefined', 'null', ''].includes(value), + { + params: { + errorMessage: + 'The value must be a string that ends with .json or .svg' + } + } + ) + .transform((value) => + !['undefined', 'null', ''].includes(value) ? value : null + ) + .nullable(); + }, + + /** + * The `instr` validator that returns a Zod schema. + * + * The validation schema ensures the same work as the `options` validator. + * + * @function instr + * + * @returns {z.ZodSchema} A Zod schema object for validating the `instr` + * option. + */ + instr() { + return v.chartConfig(); + }, + + /** + * The `options` validator that returns a Zod schema. + * + * The validation schema ensures the same work as the `options` validator. + * + * @function options + * + * @returns {z.ZodSchema} A Zod schema object for validating the `options` + * option. + */ + options() { + return v.chartConfig(); + }, + + /** + * The `svg` validator that returns a Zod schema. + * + * The validation schema ensures that the schema will accept object values + * or trimmed string values that contain ' + value.indexOf('= 0 || + value.indexOf('= 0 || + ['false', 'undefined', 'null', ''].includes(value), + { + params: { + errorMessage: + "The value must be a string that contains ' + !['false', 'undefined', 'null', ''].includes(value) ? value : null + ) + .nullable(); + }, + + /** + * The `outfile` validator that returns a Zod schema with an optional stricter + * check based on the `strictCheck` parameter. + * + * The validation schema ensures that: + * + * - When `strictCheck` is true, the schema will accept trimmed string values + * that end with '.jpeg', '.jpg', '.png', '.pdf', or '.svg', are at least one + * character long excluding the extension, or null. + * + * - When `strictCheck` is false, the schema will accept trimmed string values + * that end with '.jpeg', '.jpg', '.png', '.pdf', or '.svg', are at least one + * character long excluding the extension and will be null if the provided + * value is null, 'undefined', 'null', or ''. + * + * @function outfile + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `outfile` + * option. + */ + outfile(strictCheck) { + return strictCheck + ? z + .string() + .trim() + .refine( + (value) => + (value.length >= 6 && value.endsWith('.jpeg')) || + (value.length >= 5 && + (value.endsWith('.jpg') || + value.endsWith('.png') || + value.endsWith('.pdf') || + value.endsWith('.svg'))), + { + params: { + errorMessage: + 'The value must be a string that ends with .jpeg, .jpg, .png, .pdf, or .svg' + } + } + ) + .nullable() + : z + .string() + .trim() + .refine( + (value) => + (value.length >= 6 && value.endsWith('.jpeg')) || + (value.length >= 5 && + (value.endsWith('.jpg') || + value.endsWith('.png') || + value.endsWith('.pdf') || + value.endsWith('.svg'))) || + ['undefined', 'null', ''].includes(value), + { + params: { + errorMessage: + 'The value must be a string that ends with .jpeg, .jpg, .png, .pdf, or .svg' + } + } + ) + .transform((value) => + !['undefined', 'null', ''].includes(value) ? value : null + ) + .nullable(); + }, + + /** + * The `type` 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 `enum` validator. + * + * @function type + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `type` + * option. + */ + type(strictCheck) { + return v.enum(['jpeg', 'jpg', 'png', 'pdf', 'svg'], strictCheck); + }, + + /** + * The `constr` 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 `enum` validator. + * + * @function constr + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `constr` + * option. + */ + constr(strictCheck) { + return v.enum( + ['chart', 'stockChart', 'mapChart', 'ganttChart'], + strictCheck + ); + }, + + /** + * The `b64` 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 b64 + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `b64` option. + */ + b64(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `noDownload` 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 noDownload + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `noDownload` + * option. + */ + noDownload(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `defaultHeight` 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 `positiveNum` validator. + * + * @function defaultHeight + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `defaultHeight` option. + */ + defaultHeight(strictCheck) { + return v.positiveNum(strictCheck); + }, + + /** + * The `defaultWidth` 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 `positiveNum` validator. + * + * @function defaultWidth + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `defaultWidth` option. + */ + defaultWidth(strictCheck) { + return v.positiveNum(strictCheck); + }, + + /** + * The `defaultScale` validator that returns a Zod schema with an optional + * stricter check based on the `strictCheck` parameter. + * + * The validation schema ensures that: + * + * - When `strictCheck` is true, the schema will accept number values that + * are between 0.1 and 5 (inclusive). + * + * - When `strictCheck` is false, the schema will accept number values + * and stringified number values that are between 0.1 and 5 (inclusive), null, + * 'undefined', 'null', and '' which will be transformed to null. + * + * @function defaultScale + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `defaultScale` option. + */ + defaultScale(strictCheck) { + return strictCheck + ? z.number().gte(0.1).lte(5) + : z + .union([ + z + .string() + .trim() + .refine( + (value) => + (!isNaN(Number(value)) && + value !== true && + !value.startsWith('[') && + Number(value) >= 0.1 && + Number(value) <= 5) || + ['undefined', 'null', ''].includes(value), + { + params: { + errorMessage: 'The value must be within a 0.1 and 5.0 range' + } + } + ) + .transform((value) => + !['undefined', 'null', ''].includes(value) + ? Number(value) + : null + ), + z.number().gte(0.1).lte(5) + ]) + .nullable(); + }, + + /** + * The `height` validator that returns a Zod schema with an optional stricter + * check based on the `strictCheck` parameter. + * + * The validation schema ensures the same work as a nullable `defaultHeight` + * validator. + * + * @function height + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `height` + * option. + */ + height(strictCheck) { + return this.defaultHeight(strictCheck).nullable(); + }, + + /** + * The `width` validator that returns a Zod schema with an optional stricter + * check based on the `strictCheck` parameter. + * + * The validation schema ensures the same work as a nullable `defaultWidth` + * validator. + * + * @function width + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `width` + * option. + */ + width(strictCheck) { + return this.defaultWidth(strictCheck).nullable(); + }, + + /** + * The `scale` validator that returns a Zod schema with an optional stricter + * check based on the `strictCheck` parameter. + * + * The validation schema ensures the same work as a nullable `defaultScale` + * validator. + * + * @function scale + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `scale` + * option. + */ + scale(strictCheck) { + return this.defaultScale(strictCheck).nullable(); + }, + + /** + * The `globalOptions` validator that returns a Zod schema. + * + * The validation schema ensures the same work as the `additionalOptions` + * validator. + * + * @function globalOptions + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `globalOptions` option. + */ + globalOptions() { + return v.additionalOptions(); + }, + + /** + * The `themeOptions` validator that returns a Zod schema. + * + * The validation schema ensures the same work as the `additionalOptions` + * validator. + * + * @function themeOptions + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `themeOptions` option. + */ + themeOptions() { + return v.additionalOptions(); + }, + + /** + * The `batch` 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 batch + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `batch` + * option. + */ + batch(strictCheck) { + return v.string(strictCheck); + }, + + /** + * The `rasterizationTimeout` 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 rasterizationTimeout + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `rasterizationTimeout` option. + */ + rasterizationTimeout(strictCheck) { + return v.nonNegativeNum(strictCheck); + }, + + /** + * The `allowCodeExecution` 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 allowCodeExecution + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `allowCodeExecution` option. + */ + allowCodeExecution(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `allowFileResources` 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 allowFileResources + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `allowFileResources` option. + */ + allowFileResources(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `customCode` 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 customCode + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `customCode` + * option. + */ + customCode(strictCheck) { + return v.string(strictCheck); + }, + + /** + * The `callback` 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 callback + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `callback` + * option. + */ + callback(strictCheck) { + return v.string(strictCheck); + }, + + /** + * The `resources` validator that returns a Zod schema with an optional + * stricter check based on the `strictCheck` parameter. + * + * The validation schema ensures that: + * + * - When `strictCheck` is true, the schema will accept a partial object + * with allowed properties `js`, `css`, and `files` where each of the allowed + * properties can be null, stringified version of the object, string that ends + * with the '.json', and null. + * + * - When `strictCheck` is false, the schema will accept a stringified version + * of a partial object with allowed properties `js`, `css`, and `files` where + * each of the allowed properties can be null, string that ends with the + * '.json', and will be null if the provided value is 'undefined', 'null' + * or ''. + * + * @function resources + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `resources` + * option. + */ + resources(strictCheck) { + const objectSchema = z + .object({ + js: v.string(false), + css: v.string(false), + files: v + .stringArray( + (value) => !['undefined', 'null', ''].includes(value), + ',', + true + ) + .nullable() + }) + .partial(); + + const stringSchema1 = z + .string() + .trim() + .refine( + (value) => + (value.startsWith('{') && value.endsWith('}')) || + (value.length >= 6 && value.endsWith('.json')), + { + params: { + errorMessage: + "The value must be a string that starts with '{' and ends with '}" + } + } + ); + + const stringSchema2 = z + .string() + .trim() + .refine( + (value) => + (value.startsWith('{') && value.endsWith('}')) || + (value.length >= 6 && value.endsWith('.json')) || + ['undefined', 'null', ''].includes(value), + { + params: { + errorMessage: 'The value must be a string that ends with .json' + } + } + ) + .transform((value) => + !['undefined', 'null', ''].includes(value) ? value : null + ); + + return strictCheck + ? z.union([objectSchema, stringSchema1]).nullable() + : z.union([objectSchema, stringSchema2]).nullable(); + }, + + /** + * The `loadConfig` 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. + * Additionally, it must be a string that ends with '.json'. + * + * @function loadConfig + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `loadConfig` + * option. + */ + loadConfig(strictCheck) { + return v + .string(strictCheck) + .refine( + (value) => + value === null || (value.length >= 6 && value.endsWith('.json')), + { + params: { + errorMessage: 'The value must be a string that ends with .json' + } + } + ); + }, + + /** + * The `createConfig` 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 `loadConfig` validator. + * + * @function createConfig + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `createConfig` option. + */ + createConfig(strictCheck) { + return this.loadConfig(strictCheck); + }, + + /** + * The `enableServer` 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 enableServer + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `enableServer` option. + */ + enableServer(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `host` 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 host + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `host` + * option. + */ + host(strictCheck) { + return v.string(strictCheck); + }, + + /** + * The `port` 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 port + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `port` + * option. + */ + port(strictCheck) { + return v.nonNegativeNum(strictCheck); + }, + + /** + * The `uploadLimit` 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 `positiveNum` validator. + * + * @function uploadLimit + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `uploadLimit` + * option. + */ + uploadLimit(strictCheck) { + return v.positiveNum(strictCheck); + }, + + /** + * The `serverBenchmarking` 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 serverBenchmarking + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `serverBenchmarking` option. + */ + serverBenchmarking(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `proxyHost` 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 proxyHost + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `proxyHost` + * option. + */ + proxyHost(strictCheck) { + return v.string(strictCheck); + }, + + /** + * The `proxyPort` validator that returns a Zod schema with an optional + * stricter check based on the `strictCheck` parameter. + * + * The validation schema ensures the same work as a nullable `nonNegativeNum` + * validator. + * + * @function proxyPort + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `proxyPort` + * option. + */ + proxyPort(strictCheck) { + return v.nonNegativeNum(strictCheck).nullable(); + }, + + /** + * The `proxyTimeout` 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 proxyTimeout + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `proxyTimeout` option. + */ + proxyTimeout(strictCheck) { + return v.nonNegativeNum(strictCheck); + }, + + /** + * The `enableRateLimiting` 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 enableRateLimiting + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `enableRateLimiting` option. + */ + enableRateLimiting(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `maxRequests` 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 maxRequests + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `maxRequests` + * option. + */ + maxRequests(strictCheck) { + return v.nonNegativeNum(strictCheck); + }, + + /** + * The `window` 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 window + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `window` + * option. + */ + window(strictCheck) { + return v.nonNegativeNum(strictCheck); + }, + + /** + * The `delay` 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 delay + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `delay` + * option. + */ + delay(strictCheck) { + return v.nonNegativeNum(strictCheck); + }, + + /** + * The `trustProxy` 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 trustProxy + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `trustProxy` + * option. + */ + trustProxy(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `skipKey` 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 skipKey + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `skipKey` + * option. + */ + skipKey(strictCheck) { + return v.string(strictCheck); + }, + + /** + * The `skipToken` 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 skipToken + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `skipToken` + * option. + */ + skipToken(strictCheck) { + return v.string(strictCheck); + }, + + /** + * The `enableSsl` 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 enableSsl + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `enableSsl` + * option. + */ + enableSsl(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `sslForce` 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 sslForce + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `sslForce` + * option. + */ + sslForce(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `sslPort` 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 sslPort + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `sslPort` + * option. + */ + sslPort(strictCheck) { + return v.nonNegativeNum(strictCheck); + }, + + /** + * The `sslCertPath` 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 sslCertPath + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `sslCertPath` + * option. + */ + sslCertPath(strictCheck) { + return v.string(strictCheck); + }, + + /** + * The `minWorkers` 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 `positiveNum` validator. + * + * @function minWorkers + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `minWorkers` + * option. + */ + minWorkers(strictCheck) { + return v.positiveNum(strictCheck); + }, + + /** + * The `maxWorkers` 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 `positiveNum` validator. + * + * @function maxWorkers + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `maxWorkers` + * option. + */ + maxWorkers(strictCheck) { + return v.positiveNum(strictCheck); + }, + + /** + * The `workLimit` 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 `positiveNum` validator. + * + * @function workLimit + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `workLimit` + * option. + */ + workLimit(strictCheck) { + return v.positiveNum(strictCheck); + }, + + /** + * The `acquireTimeout` 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 acquireTimeout + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `acquireTimeout` option. + */ + acquireTimeout(strictCheck) { + return v.nonNegativeNum(strictCheck); + }, + + /** + * The `createTimeout` 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 createTimeout + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `createTimeout` option. + */ + createTimeout(strictCheck) { + return v.nonNegativeNum(strictCheck); + }, + + /** + * The `destroyTimeout` 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 destroyTimeout + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `destroyTimeout` option. + */ + destroyTimeout(strictCheck) { + return v.nonNegativeNum(strictCheck); + }, + + /** + * The `idleTimeout` 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 idleTimeout + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `idleTimeout` option. + */ + idleTimeout(strictCheck) { + return v.nonNegativeNum(strictCheck); + }, + + /** + * The `createRetryInterval` 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 createRetryInterval + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `createRetryInterval` option. + */ + createRetryInterval(strictCheck) { + return v.nonNegativeNum(strictCheck); + }, + + /** + * The `reaperInterval` 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 reaperInterval + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `reaperInterval` option. + */ + reaperInterval(strictCheck) { + return v.nonNegativeNum(strictCheck); + }, + + /** + * The `poolBenchmarking` 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 poolBenchmarking + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `poolBenchmarking` option. + */ + poolBenchmarking(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `resourcesInterval` 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 resourcesInterval + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `resourcesInterval` option. + */ + resourcesInterval(strictCheck) { + return v.nonNegativeNum(strictCheck); + }, + + /** + * The `logLevel` validator that returns a Zod schema with an optional + * stricter check based on the `strictCheck` parameter. + * + * The validation schema ensures that: + * + * - When `strictCheck` is true, the schema will accept integer number values + * that are between 0 and 5 (inclusive). + * + * - When `strictCheck` is false, the schema will accept integer number values + * and stringified integer number values that are between 1 and 5 (inclusive), + * null, 'undefined', 'null', and '' which will be transformed to null. + * + * @function logLevel + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `logLevel` + * option. + */ + logLevel(strictCheck) { + return strictCheck + ? z.number().int().gte(0).lte(5) + : z + .union([ + z + .string() + .trim() + .refine( + (value) => + (!isNaN(Number(value)) && + value !== true && + !value.startsWith('[') && + Number.isInteger(Number(value)) && + Number(value) >= 0 && + Number(value) <= 5) || + ['undefined', 'null', ''].includes(value), + { + params: { + errorMessage: 'The value must be within a 0 and 5 range' + } + } + ) + .transform((value) => + !['undefined', 'null', ''].includes(value) + ? Number(value) + : null + ), + z.number().int().gte(0).lte(5) + ]) + .nullable(); + }, + + /** + * The `logFile` 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. + * Additionally, it must be a string that ends with '.log'. + * + * @function logFile + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `logFile` + * option. + */ + logFile(strictCheck) { + return v + .string(strictCheck) + .refine( + (value) => + value === null || (value.length >= 5 && value.endsWith('.log')), + { + params: { + errorMessage: 'The value must be a string that ends with .log' + } + } + ); + }, + + /** + * The `logDest` 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 logDest + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `logDest` + * option. + */ + logDest(strictCheck) { + return v.string(strictCheck); + }, + + /** + * The `logToConsole` 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 logToConsole + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `logToConsole` option. + */ + logToConsole(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `logToFile` 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 logToFile + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `logToFile` + * option. + */ + logToFile(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `enableUi` 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 enableUi + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `enableUi` + * option. + */ + enableUi(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `uiRoute` 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 uiRoute + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `uiRoute` + * option. + */ + uiRoute(strictCheck) { + return v.startsWith(['/'], strictCheck); + }, + + /** + * The `nodeEnv` 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 `enum` validator. + * + * @function nodeEnv + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `nodeEnv` + * option. + */ + nodeEnv(strictCheck) { + return v.enum(['development', 'production', 'test'], strictCheck); + }, + + /** + * The `listenToProcessExits` 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 listenToProcessExits + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `listenToProcessExits` option. + */ + listenToProcessExits(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `noLogo` 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 noLogo + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `noLogo` + * option. + */ + noLogo(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `hardResetPage` 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 hardResetPage + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `hardResetPage` option. + */ + hardResetPage(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `browserShellMode` 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 browserShellMode + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `browserShellMode` option. + */ + browserShellMode(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `validation` 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 validation + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `validation` + * option. + */ + validation(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `enableDebug` 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 enableDebug + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `enableDebug` + * option. + */ + enableDebug(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `headless` 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 headless + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `headless` + * option. + */ + headless(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `devtools` 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 devtools + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `devtools` + * option. + */ + devtools(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `listenToConsole` 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 listenToConsole + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `listenToConsole` option. + */ + listenToConsole(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `dumpio` 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 dumpio + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `dumpio` + * option. + */ + dumpio(strictCheck) { + return v.boolean(strictCheck); + }, + + /** + * The `slowMo` 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 slowMo + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `slowMo` + * option. + */ + slowMo(strictCheck) { + return v.nonNegativeNum(strictCheck); + }, + + /** + * The `debuggingPort` 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 debuggingPort + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating + * the `debuggingPort` option. + */ + debuggingPort(strictCheck) { + return v.nonNegativeNum(strictCheck); + }, + + /** + * The `requestId` 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. + * Additionally, it must be a stringified UUID or can be null. + * + * @function requestId + * + * @param {boolean} strictCheck - Determines if stricter validation should + * be applied. + * + * @returns {z.ZodSchema} A Zod schema object for validating the `requestId` + * option. + */ + requestId() { + return z + .string() + .uuid({ message: 'The value must be a stringified UUID' }) + .nullable(); + } +}; + +// Schema for the puppeteer section of options +const PuppeteerSchema = (strictCheck) => + z + .object({ + args: validators.args(strictCheck) + }) + .partial(); + +// Schema for the highcharts section of options +const HighchartsSchema = (strictCheck) => + z + .object({ + version: validators.version(strictCheck), + cdnUrl: validators.cdnUrl(strictCheck), + forceFetch: validators.forceFetch(strictCheck), + cachePath: validators.cachePath(strictCheck), + coreScripts: validators.coreScripts(strictCheck), + moduleScripts: validators.moduleScripts(strictCheck), + indicatorScripts: validators.indicatorScripts(strictCheck), + customScripts: validators.customScripts(strictCheck) + }) + .partial(); + +// Schema for the export section of options +const ExportSchema = (strictCheck) => + z + .object({ + infile: validators.infile(strictCheck), + instr: validators.instr(), + options: validators.options(), + svg: validators.svg(), + outfile: validators.outfile(strictCheck), + type: validators.type(strictCheck), + constr: validators.constr(strictCheck), + b64: validators.b64(strictCheck), + noDownload: validators.noDownload(strictCheck), + defaultHeight: validators.defaultHeight(strictCheck), + defaultWidth: validators.defaultWidth(strictCheck), + defaultScale: validators.defaultScale(strictCheck), + height: validators.height(strictCheck), + width: validators.width(strictCheck), + scale: validators.scale(strictCheck), + globalOptions: validators.globalOptions(), + themeOptions: validators.themeOptions(), + batch: validators.batch(false), + rasterizationTimeout: validators.rasterizationTimeout(strictCheck) + }) + .partial(); + +// Schema for the customLogic section of options +const CustomLogicSchema = (strictCheck) => + z + .object({ + allowCodeExecution: validators.allowCodeExecution(strictCheck), + allowFileResources: validators.allowFileResources(strictCheck), + customCode: validators.customCode(false), + callback: validators.callback(false), + resources: validators.resources(strictCheck), + loadConfig: validators.loadConfig(false), + createConfig: validators.createConfig(false) + }) + .partial(); + +// Schema for the server.proxy section of options +const ProxySchema = (strictCheck) => + z + .object({ + host: validators.proxyHost(false), + port: validators.proxyPort(strictCheck), + timeout: validators.proxyTimeout(strictCheck) + }) + .partial(); + +// Schema for the server.rateLimiting section of options +const RateLimitingSchema = (strictCheck) => + z + .object({ + enable: validators.enableRateLimiting(strictCheck), + maxRequests: validators.maxRequests(strictCheck), + window: validators.window(strictCheck), + delay: validators.delay(strictCheck), + trustProxy: validators.trustProxy(strictCheck), + skipKey: validators.skipKey(false), + skipToken: validators.skipToken(false) + }) + .partial(); + +// Schema for the server.ssl section of options +const SslSchema = (strictCheck) => + z + .object({ + enable: validators.enableSsl(strictCheck), + force: validators.sslForce(strictCheck), + port: validators.sslPort(strictCheck), + certPath: validators.sslCertPath(false) + }) + .partial(); + +// Schema for the server section of options +const ServerSchema = (strictCheck) => + z.object({ + enable: validators.enableServer(strictCheck).optional(), + host: validators.host(strictCheck).optional(), + port: validators.port(strictCheck).optional(), + uploadLimit: validators.uploadLimit(strictCheck).optional(), + benchmarking: validators.serverBenchmarking(strictCheck).optional(), + proxy: ProxySchema(strictCheck).optional(), + rateLimiting: RateLimitingSchema(strictCheck).optional(), + ssl: SslSchema(strictCheck).optional() + }); + +// Schema for the pool section of options +const PoolSchema = (strictCheck) => + z + .object({ + minWorkers: validators.minWorkers(strictCheck), + maxWorkers: validators.maxWorkers(strictCheck), + workLimit: validators.workLimit(strictCheck), + acquireTimeout: validators.acquireTimeout(strictCheck), + createTimeout: validators.createTimeout(strictCheck), + destroyTimeout: validators.destroyTimeout(strictCheck), + idleTimeout: validators.idleTimeout(strictCheck), + createRetryInterval: validators.createRetryInterval(strictCheck), + reaperInterval: validators.reaperInterval(strictCheck), + benchmarking: validators.poolBenchmarking(strictCheck) + }) + .partial(); + +// Schema for the logging section of options +const LoggingSchema = (strictCheck) => + z + .object({ + level: validators.logLevel(strictCheck), + file: validators.logFile(strictCheck), + dest: validators.logDest(strictCheck), + toConsole: validators.logToConsole(strictCheck), + toFile: validators.logToFile(strictCheck) + }) + .partial(); + +// Schema for the ui section of options +const UiSchema = (strictCheck) => + z + .object({ + enable: validators.enableUi(strictCheck), + route: validators.uiRoute(strictCheck) + }) + .partial(); + +// Schema for the other section of options +const OtherSchema = (strictCheck) => + z + .object({ + nodeEnv: validators.nodeEnv(strictCheck), + listenToProcessExits: validators.listenToProcessExits(strictCheck), + noLogo: validators.noLogo(strictCheck), + hardResetPage: validators.hardResetPage(strictCheck), + browserShellMode: validators.browserShellMode(strictCheck), + validation: validators.validation(strictCheck) + }) + .partial(); + +// Schema for the debug section of options +const DebugSchema = (strictCheck) => + z + .object({ + enable: validators.enableDebug(strictCheck), + headless: validators.headless(strictCheck), + devtools: validators.devtools(strictCheck), + listenToConsole: validators.listenToConsole(strictCheck), + dumpio: validators.dumpio(strictCheck), + slowMo: validators.slowMo(strictCheck), + debuggingPort: validators.debuggingPort(strictCheck) + }) + .partial(); + +// Strict schema for the config +export const StrictConfigSchema = z.object({ + requestId: validators.requestId(), + puppeteer: PuppeteerSchema(true), + highcharts: HighchartsSchema(true), + export: ExportSchema(true), + customLogic: CustomLogicSchema(true), + server: ServerSchema(true), + pool: PoolSchema(true), + logging: LoggingSchema(true), + ui: UiSchema(true), + other: OtherSchema(true), + debug: DebugSchema(true) +}); + +// Loose schema for the config +export const LooseConfigSchema = z.object({ + requestId: validators.requestId(), + puppeteer: PuppeteerSchema(false), + highcharts: HighchartsSchema(false), + export: ExportSchema(false), + customLogic: CustomLogicSchema(false), + server: ServerSchema(false), + pool: PoolSchema(false), + logging: LoggingSchema(false), + ui: UiSchema(false), + other: OtherSchema(false), + debug: DebugSchema(false) +}); + +// Schema for the environment variables config +export const EnvSchema = z.object({ + // puppeteer + PUPPETEER_ARGS: validators.args(false), + + // highcharts + HIGHCHARTS_VERSION: validators.version(false), + HIGHCHARTS_CDN_URL: validators.cdnUrl(false), + HIGHCHARTS_FORCE_FETCH: validators.forceFetch(false), + HIGHCHARTS_CACHE_PATH: validators.cachePath(false), + HIGHCHARTS_ADMIN_TOKEN: validators.adminToken(false), + HIGHCHARTS_CORE_SCRIPTS: validators.coreScripts(false), + HIGHCHARTS_MODULE_SCRIPTS: validators.moduleScripts(false), + HIGHCHARTS_INDICATOR_SCRIPTS: validators.indicatorScripts(false), + HIGHCHARTS_CUSTOM_SCRIPTS: validators.customScripts(false), + + // export + EXPORT_INFILE: validators.infile(false), + EXPORT_INSTR: validators.instr(), + EXPORT_OPTIONS: validators.options(), + EXPORT_SVG: validators.svg(), + EXPORT_BATCH: validators.batch(false), + EXPORT_OUTFILE: validators.outfile(false), + EXPORT_TYPE: validators.type(false), + EXPORT_CONSTR: validators.constr(false), + EXPORT_B64: validators.b64(false), + EXPORT_NO_DOWNLOAD: validators.noDownload(false), + EXPORT_HEIGHT: validators.height(false), + EXPORT_WIDTH: validators.width(false), + EXPORT_SCALE: validators.scale(false), + EXPORT_DEFAULT_HEIGHT: validators.defaultHeight(false), + EXPORT_DEFAULT_WIDTH: validators.defaultWidth(false), + EXPORT_DEFAULT_SCALE: validators.defaultScale(false), + EXPORT_GLOBAL_OPTIONS: validators.globalOptions(), + EXPORT_THEME_OPTIONS: validators.themeOptions(), + EXPORT_RASTERIZATION_TIMEOUT: validators.rasterizationTimeout(false), + + // custom + CUSTOM_LOGIC_ALLOW_CODE_EXECUTION: validators.allowCodeExecution(false), + CUSTOM_LOGIC_ALLOW_FILE_RESOURCES: validators.allowFileResources(false), + CUSTOM_LOGIC_CUSTOM_CODE: validators.customCode(false), + CUSTOM_LOGIC_CALLBACK: validators.callback(false), + CUSTOM_LOGIC_RESOURCES: validators.resources(false), + CUSTOM_LOGIC_LOAD_CONFIG: validators.loadConfig(false), + CUSTOM_LOGIC_CREATE_CONFIG: validators.createConfig(false), + + // server + SERVER_ENABLE: validators.enableServer(false), + SERVER_HOST: validators.host(false), + SERVER_PORT: validators.port(false), + SERVER_UPLOAD_LIMIT: validators.uploadLimit(false), + SERVER_BENCHMARKING: validators.serverBenchmarking(false), + + // server proxy + SERVER_PROXY_HOST: validators.proxyHost(false), + SERVER_PROXY_PORT: validators.proxyPort(false), + SERVER_PROXY_TIMEOUT: validators.proxyTimeout(false), + + // server rate limiting + SERVER_RATE_LIMITING_ENABLE: validators.enableRateLimiting(false), + SERVER_RATE_LIMITING_MAX_REQUESTS: validators.maxRequests(false), + SERVER_RATE_LIMITING_WINDOW: validators.window(false), + SERVER_RATE_LIMITING_DELAY: validators.delay(false), + SERVER_RATE_LIMITING_TRUST_PROXY: validators.trustProxy(false), + SERVER_RATE_LIMITING_SKIP_KEY: validators.skipKey(false), + SERVER_RATE_LIMITING_SKIP_TOKEN: validators.skipToken(false), + + // server ssl + SERVER_SSL_ENABLE: validators.enableSsl(false), + SERVER_SSL_FORCE: validators.sslForce(false), + SERVER_SSL_PORT: validators.sslPort(false), + SERVER_SSL_CERT_PATH: validators.sslCertPath(false), + + // pool + POOL_MIN_WORKERS: validators.minWorkers(false), + POOL_MAX_WORKERS: validators.maxWorkers(false), + POOL_WORK_LIMIT: validators.workLimit(false), + POOL_ACQUIRE_TIMEOUT: validators.acquireTimeout(false), + POOL_CREATE_TIMEOUT: validators.createTimeout(false), + POOL_DESTROY_TIMEOUT: validators.destroyTimeout(false), + POOL_IDLE_TIMEOUT: validators.idleTimeout(false), + POOL_CREATE_RETRY_INTERVAL: validators.createRetryInterval(false), + POOL_REAPER_INTERVAL: validators.reaperInterval(false), + POOL_BENCHMARKING: validators.poolBenchmarking(false), + + // logging + LOGGING_LEVEL: validators.logLevel(false), + LOGGING_FILE: validators.logFile(false), + LOGGING_DEST: validators.logDest(false), + LOGGING_TO_CONSOLE: validators.logToConsole(false), + LOGGING_TO_FILE: validators.logToFile(false), + + // ui + UI_ENABLE: validators.enableUi(false), + UI_ROUTE: validators.uiRoute(false), + + // other + OTHER_NODE_ENV: validators.nodeEnv(false), + OTHER_LISTEN_TO_PROCESS_EXITS: validators.listenToProcessExits(false), + OTHER_NO_LOGO: validators.noLogo(false), + OTHER_HARD_RESET_PAGE: validators.hardResetPage(false), + OTHER_BROWSER_SHELL_MODE: validators.browserShellMode(false), + OTHER_VALIDATION: validators.validation(false), + + // debugger + DEBUG_ENABLE: validators.enableDebug(false), + DEBUG_HEADLESS: validators.headless(false), + DEBUG_DEVTOOLS: validators.devtools(false), + DEBUG_LISTEN_TO_CONSOLE: validators.listenToConsole(false), + DEBUG_DUMPIO: validators.dumpio(false), + DEBUG_SLOW_MO: validators.slowMo(false), + DEBUG_DEBUGGING_PORT: validators.debuggingPort(false) +}); + +/** + * Validates the environment variables options using the EnvSchema. + * + * @param {Object} process.env - The configuration options from environment + * variables file to validate. + * + * @returns {Object} The parsed and validated environment variables. + */ +export const envs = EnvSchema.partial().parse(process.env); + +/** + * Validates the configuration options using the `StrictConfigSchema`. + * + * @function strictValidate + * + * @param {Object} configOptions - The configuration options to validate. + * + * @returns {Object} The parsed and validated configuration options. + */ +export function strictValidate(configOptions) { + return StrictConfigSchema.partial().parse(configOptions); +} + +/** + * Validates the configuration options using the `LooseConfigSchema`. + * + * @function looseValidate + * + * @param {Object} configOptions - The configuration options to validate. + * + * @returns {Object} The parsed and validated configuration options. + */ +export function looseValidate(configOptions) { + return LooseConfigSchema.partial().parse(configOptions); +} + +/** + * Custom error mapping function for Zod schema validation. + * + * This function customizes the error messages produced by Zod schema + * validation, providing more specific and user-friendly feedback based on the + * issue type and context. + * + * The function modifies the error messages as follows: + * + * - For missing required values (undefined), it returns a message indicating + * that no value was provided for the specific property. + * + * - For custom validation errors, if a custom error message is provided in the + * issue parameters, it includes this message along with the invalid data + * received. + * + * - For all other errors, it appends property-specific information to the + * default error message provided by Zod. + * + * @function _customErrorMap + * + * @param {z.ZodIssue} issue - The issue object representing the validation + * error. + * @param {Object} context - The context object providing additional information + * about the validation error. + * + * @returns {Object} An object containing the customized error message. + */ +function _customErrorMap(issue, context) { + // Get the chain of properties which error directly refers to + const propertyName = issue.path.join('.'); + + // Create the first part of the message about the property information + const propertyInfo = `Invalid value for the ${propertyName}`; + + // Modified message for the invalid type + if (issue.code === z.ZodIssueCode.invalid_type) { + // Modified message for the required values + if (issue.received === z.ZodParsedType.undefined) { + return { + message: `${propertyInfo} - No value was provided.` + }; + } + + // Modified message for the specific invalid type when values exist + return { + message: `${propertyInfo} - Invalid type. ${context.defaultError}.` + }; + } + + // Modified message for the custom validation + if (issue.code === z.ZodIssueCode.custom) { + // If the custom message for error exist, include it + if (issue.params?.errorMessage) { + return { + message: `${propertyInfo} - ${issue.params?.errorMessage}, received '${context.data}'.` + }; + } + } + + // Modified message for the invalid union error + if (issue.code === z.ZodIssueCode.invalid_union) { + // Create the first part of the message about the multiple errors + let message = `Multiple errors occurred for the ${propertyName}:\n`; + + // Cycle through all errors and create a correct message + issue.unionErrors.forEach((value) => { + const index = value.issues[0].message.indexOf('-'); + message += + index !== -1 + ? `${value.issues[0].message}\n`.substring(index) + : `${value.issues[0].message}\n`; + }); + + // Return the final message for the invalid union error + return { + message + }; + } + + // Return the default error message, extended by the info about the property + return { + message: `${propertyInfo} - ${context.defaultError}.` + }; +} + +export default { + validators, + StrictConfigSchema, + LooseConfigSchema, + EnvSchema, + envs, + strictValidate, + looseValidate +}; diff --git a/tests/unit/envs.test.js b/tests/unit/envs.test.js deleted file mode 100644 index 0cd71622..00000000 --- a/tests/unit/envs.test.js +++ /dev/null @@ -1,78 +0,0 @@ -/******************************************************************************* - -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. - -*******************************************************************************/ - -import { describe, expect } from '@jest/globals'; - -import { Config } from '../../lib/envs.js'; - -describe('Environment variables should be correctly parsed', () => { - test('HIGHCHARTS_VERSION accepts latests and not unrelated strings', () => { - const env = { HIGHCHARTS_VERSION: 'string-other-than-latest' }; - expect(() => Config.partial().parse(env)).toThrow(); - - env.HIGHCHARTS_VERSION = 'latest'; - expect(Config.partial().parse(env).HIGHCHARTS_VERSION).toEqual('latest'); - }); - - test('HIGHCHARTS_VERSION accepts proper version strings like XX.YY.ZZ', () => { - const env = { HIGHCHARTS_VERSION: '11' }; - expect(Config.partial().parse(env).HIGHCHARTS_VERSION).toEqual('11'); - - env.HIGHCHARTS_VERSION = '11.0.0'; - expect(Config.partial().parse(env).HIGHCHARTS_VERSION).toEqual('11.0.0'); - - env.HIGHCHARTS_VERSION = '9.1'; - expect(Config.partial().parse(env).HIGHCHARTS_VERSION).toEqual('9.1'); - - env.HIGHCHARTS_VERSION = '11a.2.0'; - expect(() => Config.partial().parse(env)).toThrow(); - }); - - test('HIGHCHARTS_CDN_URL should start with http:// or https://', () => { - const env = { HIGHCHARTS_CDN_URL: 'http://example.com' }; - expect(Config.partial().parse(env).HIGHCHARTS_CDN_URL).toEqual( - 'http://example.com' - ); - - env.HIGHCHARTS_CDN_URL = 'https://example.com'; - expect(Config.partial().parse(env).HIGHCHARTS_CDN_URL).toEqual( - 'https://example.com' - ); - - env.HIGHCHARTS_CDN_URL = 'example.com'; - expect(() => Config.partial().parse(env)).toThrow(); - }); - - test('CORE, MODULE, INDICATOR scripts should be arrays', () => { - const env = { - HIGHCHARTS_CORE_SCRIPTS: 'core1, core2, highcharts', - HIGHCHARTS_MODULE_SCRIPTS: 'module1, map, module2', - HIGHCHARTS_INDICATOR_SCRIPTS: 'indicators-all, indicator1, indicator2' - }; - - const parsed = Config.partial().parse(env); - - expect(parsed.HIGHCHARTS_CORE_SCRIPTS).toEqual(['highcharts']); - expect(parsed.HIGHCHARTS_MODULE_SCRIPTS).toEqual(['map']); - expect(parsed.HIGHCHARTS_INDICATOR_SCRIPTS).toEqual(['indicators-all']); - }); - - test('HIGHCHARTS_FORCE_FETCH should be a boolean', () => { - const env = { HIGHCHARTS_FORCE_FETCH: 'true' }; - expect(Config.partial().parse(env).HIGHCHARTS_FORCE_FETCH).toEqual(true); - - env.HIGHCHARTS_FORCE_FETCH = 'false'; - expect(Config.partial().parse(env).HIGHCHARTS_FORCE_FETCH).toEqual(false); - }); -}); diff --git a/tests/unit/validation/cli.test.js b/tests/unit/validation/cli.test.js new file mode 100644 index 00000000..ad92238c --- /dev/null +++ b/tests/unit/validation/cli.test.js @@ -0,0 +1,661 @@ +/******************************************************************************* + +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. + +*******************************************************************************/ + +import { describe } from '@jest/globals'; + +import { configTests } from './shared.js'; +import { LooseConfigSchema } from '../../../lib/validation.js'; + +describe('CLI options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(LooseConfigSchema.partial(), false); + + // requestId + tests.requestId('requestId'); + + // puppeteer + tests.puppeteer('puppeteer', { + args: [ + '--allow-running-insecure-content', + '--ash-no-nudges', + '--autoplay-policy=user-gesture-required', + '--block-new-web-contents', + '--disable-accelerated-2d-canvas', + '--disable-background-networking', + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-breakpad', + '--disable-checker-imaging', + '--disable-client-side-phishing-detection', + '--disable-component-extensions-with-background-pages', + '--disable-component-update', + '--disable-default-apps', + '--disable-dev-shm-usage', + '--disable-domain-reliability', + '--disable-extensions', + '--disable-features=CalculateNativeWinOcclusion,InterestFeedContentSuggestions,WebOTP', + '--disable-hang-monitor', + '--disable-ipc-flooding-protection', + '--disable-logging', + '--disable-notifications', + '--disable-offer-store-unmasked-wallet-cards', + '--disable-popup-blocking', + '--disable-print-preview', + '--disable-prompt-on-repost', + '--disable-renderer-backgrounding', + '--disable-search-engine-choice-screen', + '--disable-session-crashed-bubble', + '--disable-setuid-sandbox', + '--disable-site-isolation-trials', + '--disable-speech-api', + '--disable-sync', + '--enable-unsafe-webgpu', + '--hide-crash-restore-bubble', + '--hide-scrollbars', + '--metrics-recording-only', + '--mute-audio', + '--no-default-browser-check', + '--no-first-run', + '--no-pings', + '--no-sandbox', + '--no-startup-window', + '--no-zygote', + '--password-store=basic', + '--process-per-tab', + '--use-mock-keychain' + ] + }); + + // highcharts + tests.highcharts('highcharts', { + version: 'latest', + cdnUrl: 'https://code.highcharts.com', + forceFetch: false, + cachePath: '.cache', + coreScripts: ['highcharts', 'highcharts-more', 'highcharts-3d'], + moduleScripts: [ + 'stock', + 'map', + 'gantt', + 'exporting', + 'parallel-coordinates', + 'accessibility', + // 'annotations-advanced', + 'boost-canvas', + 'boost', + 'data', + 'data-tools', + 'draggable-points', + 'static-scale', + 'broken-axis', + 'heatmap', + 'tilemap', + 'tiledwebmap', + 'timeline', + 'treemap', + 'treegraph', + 'item-series', + 'drilldown', + 'histogram-bellcurve', + 'bullet', + 'funnel', + 'funnel3d', + 'geoheatmap', + 'pyramid3d', + 'networkgraph', + 'overlapping-datalabels', + 'pareto', + 'pattern-fill', + 'pictorial', + 'price-indicator', + 'sankey', + 'arc-diagram', + 'dependency-wheel', + 'series-label', + 'series-on-point', + 'solid-gauge', + 'sonification', + // 'stock-tools', + 'streamgraph', + 'sunburst', + 'variable-pie', + 'variwide', + 'vector', + 'venn', + 'windbarb', + 'wordcloud', + 'xrange', + 'no-data-to-display', + 'drag-panes', + 'debugger', + 'dumbbell', + 'lollipop', + 'cylinder', + 'organization', + 'dotplot', + 'marker-clusters', + 'hollowcandlestick', + 'heikinashi', + 'flowmap', + 'export-data', + 'navigator', + 'textpath' + ], + indicatorScripts: ['indicators-all'], + customScripts: [ + 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js', + 'https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.45/moment-timezone-with-data.min.js' + ] + }); + + // export + tests.export('export', { + infile: null, + instr: null, + options: null, + svg: null, + outfile: null, + type: 'png', + constr: 'chart', + b64: false, + noDownload: false, + defaultHeight: 400, + defaultWidth: 600, + defaultScale: 1, + height: null, + width: null, + scale: null, + globalOptions: null, + themeOptions: null, + batch: null, + rasterizationTimeout: 1500 + }); + + // customLogic + tests.customLogic('customLogic', { + allowCodeExecution: false, + allowFileResources: false, + customCode: null, + callback: null, + resources: null, + loadConfig: null, + createConfig: null + }); + + // server + tests.server('server', { + enable: false, + host: '0.0.0.0', + port: 7801, + benchmarking: false, + proxy: { + host: null, + port: null, + timeout: 5000 + }, + rateLimiting: { + enable: false, + maxRequests: 10, + window: 1, + delay: 0, + trustProxy: false, + skipKey: null, + skipToken: null + }, + ssl: { + enable: false, + force: false, + port: 443, + certPath: null + } + }); + + // pool + tests.pool('pool', { + minWorkers: 4, + maxWorkers: 8, + workLimit: 40, + acquireTimeout: 5000, + createTimeout: 5000, + destroyTimeout: 5000, + idleTimeout: 30000, + createRetryInterval: 200, + reaperInterval: 1000, + benchmarking: false + }); + + // logging + tests.logging('logging', { + level: 4, + file: 'highcharts-export-server.log', + dest: 'log', + toConsole: true, + toFile: true + }); + + // ui + tests.ui('ui', { + enable: false, + route: '/' + }); + + // other + tests.other('other', { + nodeEnv: 'production', + listenToProcessExits: true, + noLogo: false, + hardResetPage: false, + browserShellMode: true, + validation: true + }); + + // debug + tests.debug('debug', { + enable: false, + headless: true, + devtools: false, + listenToConsole: false, + dumpio: false, + slowMo: 0, + debuggingPort: 9222 + }); +}); + +describe('Puppeteer configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(LooseConfigSchema.shape.puppeteer, false); + + // puppeteer.args + tests.puppeteerArgs( + 'args', + '--disable-sync; --enable-unsafe-webgpu; --hide-crash-restore-bubble; --hide-scrollbars; --metrics-recording-only', + [ + '--disable-sync', + '--enable-unsafe-webgpu', + '--hide-crash-restore-bubble', + '--hide-scrollbars', + '--metrics-recording-only' + ] + ); +}); + +describe('Highcharts configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(LooseConfigSchema.shape.highcharts, false); + + // highcharts.version + tests.highchartsVersion('version'); + + // highcharts.cdnUrl + tests.highchartsCdnUrl( + 'cdnUrl', + ['http://example.com', 'https://example.com'], + ['http:a.com', 'http:/b.com', 'https:c.com', 'https:/d.com'] + ); + + // highcharts.forceFetch + tests.highchartsForceFetch('forceFetch'); + + // highcharts.cachePath + tests.highchartsCachePath('cachePath'); + + // highcharts.coreScripts + tests.highchartsCoreScripts( + 'coreScripts', + 'highcharts, highcharts-more, text1, highcharts-3d, text2', + ['highcharts', 'highcharts-more', 'highcharts-3d'] + ); + + // highcharts.moduleScripts + tests.highchartsModuleScripts( + 'moduleScripts', + 'data, text1, data-tools, text2', + ['data', 'data-tools'] + ); + + // highcharts.indicatorScripts + tests.highchartsIndicatorScripts( + 'indicatorScripts', + 'text1, indicators-all, text2', + ['indicators-all'] + ); + + // highcharts.customScripts + tests.highchartsCustomScripts( + 'customScripts', + 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js, text1, https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.45/moment-timezone-with-data.min.js, text2', + [ + 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js', + 'https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.45/moment-timezone-with-data.min.js' + ] + ); +}); + +describe('Export configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(LooseConfigSchema.shape.export, false); + + // export.infile + tests.exportInfile('infile'); + + // export.instr + tests.exportInstr('instr'); + + // export.options + tests.exportOptions('options'); + + // export.svg + tests.exportSvg('svg'); + + // export.outfile + tests.exportOutfile('outfile'); + + // export.type + tests.exportType( + 'type', + ['jpeg', 'jpg', 'png', 'pdf', 'svg'], + ['json', 'txt'] + ); + + // export.constr + tests.exportConstr( + 'constr', + ['chart', 'stockChart', 'mapChart', 'ganttChart'], + ['stock', 'map', 'gantt'] + ); + + // export.b64 + tests.exportB64('b64'); + + // export.noDownload + tests.exportNoDownload('noDownload'); + + // export.defaultHeight + tests.exportDefaultHeight('defaultHeight'); + + // export.defaultWidth + tests.exportDefaultWidth('defaultWidth'); + + // export.defaultScale + tests.exportDefaultScale('defaultScale'); + + // export.height + tests.exportHeight('height'); + + // export.width + tests.exportWidth('width'); + + // export.scale + tests.exportScale('scale'); + + // export.globalOptions + tests.exportGlobalOptions('globalOptions'); + + // export.themeOptions + tests.exportThemeOptions('themeOptions'); + + // export.batch + tests.exportBatch('batch'); + + // export.rasterizationTimeout + tests.exportRasterizationTimeout('rasterizationTimeout'); +}); + +describe('Custom Logic configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(LooseConfigSchema.shape.customLogic, false); + + // customLogic.allowCodeExecution + tests.customLogicAllowCodeExecution('allowCodeExecution'); + + // customLogic.allowFileResources + tests.customLogicAllowFileResources('allowFileResources'); + + // customLogic.customCode + tests.customLogicCustomCode('customCode'); + + // customLogic.callback + tests.customLogicCallback('callback'); + + // customLogic.resources + tests.customLogicResources('resources'); + + // customLogic.loadConfig + tests.customLogicLoadConfig('loadConfig'); + + // customLogic.createConfig + tests.customLogicCreateConfig('createConfig'); +}); + +describe('Server configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(LooseConfigSchema.shape.server, false); + + // server.enable + tests.serverEnable('enable'); + + // server.host + tests.serverHost('host'); + + // server.port + tests.serverPort('port'); + + // server.benchmarking + tests.serverBenchmarking('benchmarking'); + + // server.proxy + tests.serverProxy('proxy', { + host: null, + port: null, + timeout: 5000 + }); + + // server.rateLimiting + tests.serverRateLimiting('rateLimiting', { + enable: false, + maxRequests: 10, + window: 1, + delay: 0, + trustProxy: false, + skipKey: null, + skipToken: null + }); + + // server.ssl + tests.serverSsl('ssl', { + enable: false, + force: false, + port: 443, + certPath: null + }); +}); + +describe('Server Proxy configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(LooseConfigSchema.shape.server.shape.proxy, false); + + // server.proxy.host + tests.serverProxyHost('host'); + + // server.proxy.port + tests.serverProxyPort('port'); + + // server.proxy.timeout + tests.serverProxyTimeout('timeout'); +}); + +describe('Server Rate Limiting configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests( + LooseConfigSchema.shape.server.shape.rateLimiting, + false + ); + + // server.rateLimiting.enable + tests.serverRateLimitingEnable('enable'); + + // server.rateLimiting.maxRequests + tests.serverRateLimitingMaxRequests('maxRequests'); + + // server.rateLimiting.window + tests.serverRateLimitingWindow('window'); + + // server.rateLimiting.delay + tests.serverRateLimitingDelay('delay'); + + // server.rateLimiting.trustProxy + tests.serverRateLimitingTrustProxy('trustProxy'); + + // server.rateLimiting.skipKey + tests.serverRateLimitingSkipKey('skipKey'); + + // server.rateLimiting.skipToken + tests.serverRateLimitingSkipToken('skipToken'); +}); + +describe('Server SSL configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(LooseConfigSchema.shape.server.shape.ssl, false); + + // server.ssl.enable + tests.serverSslEnable('enable'); + + // server.ssl.force + tests.serverSslForce('force'); + + // server.ssl.port + tests.serverSslPort('port'); + + // server.ssl.certPath + tests.serverSslCertPath('certPath'); +}); + +describe('Pool configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(LooseConfigSchema.shape.pool, false); + + // pool.minWorkers + tests.poolMinWorkers('minWorkers'); + + // pool.maxWorkers + tests.poolMaxWorkers('maxWorkers'); + + // pool.workLimit + tests.poolWorkLimit('workLimit'); + + // pool.acquireTimeout + tests.poolAcquireTimeout('acquireTimeout'); + + // pool.createTimeout + tests.poolCreateTimeout('createTimeout'); + + // pool.destroyTimeout + tests.poolDestroyTimeout('destroyTimeout'); + + // pool.idleTimeout + tests.poolIdleTimeout('idleTimeout'); + + // pool.createRetryInterval + tests.poolCreateRetryInterval('createRetryInterval'); + + // pool.reaperInterval + tests.poolReaperInterval('reaperInterval'); + + // pool.benchmarking + tests.poolBenchmarking('benchmarking'); +}); + +describe('Logging configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(LooseConfigSchema.shape.logging, false); + + // logging.level + tests.loggingLevel('level'); + + // logging.file + tests.loggingFile('file'); + + // logging.dest + tests.loggingDest('dest'); + + // logging.toConsole + tests.loggingToConsole('toConsole'); + + // logging.toFile + tests.loggingToFile('toFile'); +}); + +describe('UI configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(LooseConfigSchema.shape.ui, false); + + // ui.enable + tests.uiEnable('enable'); + + // ui.route + tests.uiRoute('route', ['/', '/ui'], ['ui', 'example/ui/']); +}); + +describe('Other configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(LooseConfigSchema.shape.other, false); + + // other.nodeEnv + tests.otherNodeEnv( + 'nodeEnv', + ['development', 'production', 'test'], + ['dev-env', 'prod-env', 'test-env'] + ); + + // other.listenToProcessExits + tests.otherListenToProcessExits('listenToProcessExits'); + + // other.noLogo + tests.otherNoLogo('noLogo'); + + // other.hardResetPage + tests.otherHardResetPage('hardResetPage'); + + // other.browserShellMode + tests.otherBrowserShellMode('browserShellMode'); + + // other.validation + tests.otherValidation('validation'); +}); + +describe('Debug configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(LooseConfigSchema.shape.debug, false); + + // debug.enable + tests.debugEnable('enable'); + + // debug.headless + tests.debugHeadless('headless'); + + // debug.devtools + tests.debugDevtools('devtools'); + + // debug.listenToConsole + tests.debugListenToConsole('listenToConsole'); + + // debug.dumpio + tests.debugDumpio('dumpio'); + + // debug.slowMo + tests.debugSlowMo('slowMo'); + + // debug.debuggingPort + tests.debugDebuggingPort('debuggingPort'); +}); diff --git a/tests/unit/validation/config.test.js b/tests/unit/validation/config.test.js new file mode 100644 index 00000000..3d360d90 --- /dev/null +++ b/tests/unit/validation/config.test.js @@ -0,0 +1,672 @@ +/******************************************************************************* + +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. + +*******************************************************************************/ + +import { describe } from '@jest/globals'; + +import { configTests } from './shared.js'; +import { StrictConfigSchema } from '../../../lib/validation.js'; + +describe('Configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(StrictConfigSchema.partial(), true); + + // requestId + tests.requestId('requestId'); + + // puppeteer + tests.puppeteer('puppeteer', { + args: [ + '--allow-running-insecure-content', + '--ash-no-nudges', + '--autoplay-policy=user-gesture-required', + '--block-new-web-contents', + '--disable-accelerated-2d-canvas', + '--disable-background-networking', + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-breakpad', + '--disable-checker-imaging', + '--disable-client-side-phishing-detection', + '--disable-component-extensions-with-background-pages', + '--disable-component-update', + '--disable-default-apps', + '--disable-dev-shm-usage', + '--disable-domain-reliability', + '--disable-extensions', + '--disable-features=CalculateNativeWinOcclusion,InterestFeedContentSuggestions,WebOTP', + '--disable-hang-monitor', + '--disable-ipc-flooding-protection', + '--disable-logging', + '--disable-notifications', + '--disable-offer-store-unmasked-wallet-cards', + '--disable-popup-blocking', + '--disable-print-preview', + '--disable-prompt-on-repost', + '--disable-renderer-backgrounding', + '--disable-search-engine-choice-screen', + '--disable-session-crashed-bubble', + '--disable-setuid-sandbox', + '--disable-site-isolation-trials', + '--disable-speech-api', + '--disable-sync', + '--enable-unsafe-webgpu', + '--hide-crash-restore-bubble', + '--hide-scrollbars', + '--metrics-recording-only', + '--mute-audio', + '--no-default-browser-check', + '--no-first-run', + '--no-pings', + '--no-sandbox', + '--no-startup-window', + '--no-zygote', + '--password-store=basic', + '--process-per-tab', + '--use-mock-keychain' + ] + }); + + // highcharts + tests.highcharts('highcharts', { + version: 'latest', + cdnUrl: 'https://code.highcharts.com', + forceFetch: false, + cachePath: '.cache', + coreScripts: ['highcharts', 'highcharts-more', 'highcharts-3d'], + moduleScripts: [ + 'stock', + 'map', + 'gantt', + 'exporting', + 'parallel-coordinates', + 'accessibility', + // 'annotations-advanced', + 'boost-canvas', + 'boost', + 'data', + 'data-tools', + 'draggable-points', + 'static-scale', + 'broken-axis', + 'heatmap', + 'tilemap', + 'tiledwebmap', + 'timeline', + 'treemap', + 'treegraph', + 'item-series', + 'drilldown', + 'histogram-bellcurve', + 'bullet', + 'funnel', + 'funnel3d', + 'geoheatmap', + 'pyramid3d', + 'networkgraph', + 'overlapping-datalabels', + 'pareto', + 'pattern-fill', + 'pictorial', + 'price-indicator', + 'sankey', + 'arc-diagram', + 'dependency-wheel', + 'series-label', + 'series-on-point', + 'solid-gauge', + 'sonification', + // 'stock-tools', + 'streamgraph', + 'sunburst', + 'variable-pie', + 'variwide', + 'vector', + 'venn', + 'windbarb', + 'wordcloud', + 'xrange', + 'no-data-to-display', + 'drag-panes', + 'debugger', + 'dumbbell', + 'lollipop', + 'cylinder', + 'organization', + 'dotplot', + 'marker-clusters', + 'hollowcandlestick', + 'heikinashi', + 'flowmap', + 'export-data', + 'navigator', + 'textpath' + ], + indicatorScripts: ['indicators-all'], + customScripts: [ + 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js', + 'https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.45/moment-timezone-with-data.min.js' + ] + }); + + // export + tests.export('export', { + infile: null, + instr: null, + options: null, + svg: null, + outfile: null, + type: 'png', + constr: 'chart', + b64: false, + noDownload: false, + defaultHeight: 400, + defaultWidth: 600, + defaultScale: 1, + height: null, + width: null, + scale: null, + globalOptions: null, + themeOptions: null, + batch: null, + rasterizationTimeout: 1500 + }); + + // customLogic + tests.customLogic('customLogic', { + allowCodeExecution: false, + allowFileResources: false, + customCode: null, + callback: null, + resources: null, + loadConfig: null, + createConfig: null + }); + + // server + tests.server('server', { + enable: false, + host: '0.0.0.0', + port: 7801, + benchmarking: false, + proxy: { + host: null, + port: null, + timeout: 5000 + }, + rateLimiting: { + enable: false, + maxRequests: 10, + window: 1, + delay: 0, + trustProxy: false, + skipKey: null, + skipToken: null + }, + ssl: { + enable: false, + force: false, + port: 443, + certPath: null + } + }); + + // pool + tests.pool('pool', { + minWorkers: 4, + maxWorkers: 8, + workLimit: 40, + acquireTimeout: 5000, + createTimeout: 5000, + destroyTimeout: 5000, + idleTimeout: 30000, + createRetryInterval: 200, + reaperInterval: 1000, + benchmarking: false + }); + + // logging + tests.logging('logging', { + level: 4, + file: 'highcharts-export-server.log', + dest: 'log', + toConsole: true, + toFile: true + }); + + // ui + tests.ui('ui', { + enable: false, + route: '/' + }); + + // other + tests.other('other', { + nodeEnv: 'production', + listenToProcessExits: true, + noLogo: false, + hardResetPage: false, + browserShellMode: true, + validation: true + }); + + // debug + tests.debug('debug', { + enable: false, + headless: true, + devtools: false, + listenToConsole: false, + dumpio: false, + slowMo: 0, + debuggingPort: 9222 + }); +}); + +describe('Puppeteer configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(StrictConfigSchema.shape.puppeteer, true); + + // puppeteer.args + tests.puppeteerArgs( + 'args', + [ + '--disable-sync', + '--enable-unsafe-webgpu', + '--hide-crash-restore-bubble', + '--hide-scrollbars', + '--metrics-recording-only' + ], + [ + '--disable-sync', + '--enable-unsafe-webgpu', + '--hide-crash-restore-bubble', + '--hide-scrollbars', + '--metrics-recording-only' + ] + ); +}); + +describe('Highcharts configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(StrictConfigSchema.shape.highcharts, true); + + // highcharts.version + tests.highchartsVersion('version'); + + // highcharts.cdnUrl + tests.highchartsCdnUrl( + 'cdnUrl', + ['http://example.com', 'https://example.com'], + ['http:a.com', 'http:/b.com', 'https:c.com', 'https:/d.com'] + ); + + // highcharts.forceFetch + tests.highchartsForceFetch('forceFetch'); + + // highcharts.cachePath + tests.highchartsCachePath('cachePath'); + + // highcharts.coreScripts + tests.highchartsCoreScripts( + 'coreScripts', + ['highcharts', 'highcharts-more', 'text1', 'highcharts-3d', 'text2'], + ['highcharts', 'highcharts-more', 'highcharts-3d'] + ); + + // highcharts.moduleScripts + tests.highchartsModuleScripts( + 'moduleScripts', + ['data', 'text1', 'data-tools', 'text2'], + ['data', 'data-tools'] + ); + + // highcharts.indicatorScripts + tests.highchartsIndicatorScripts( + 'indicatorScripts', + ['text1', 'indicators-all', 'text2'], + ['indicators-all'] + ); + + // highcharts.customScripts + tests.highchartsCustomScripts( + 'customScripts', + [ + 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js', + 'text1', + 'https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.45/moment-timezone-with-data.min.js', + 'text2' + ], + [ + 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js', + 'https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.45/moment-timezone-with-data.min.js' + ] + ); +}); + +describe('Export configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(StrictConfigSchema.shape.export, true); + + // export.infile + tests.exportInfile('infile'); + + // export.instr + tests.exportInstr('instr'); + + // export.options + tests.exportOptions('options'); + + // export.svg + tests.exportSvg('svg'); + + // export.outfile + tests.exportOutfile('outfile'); + + // export.type + tests.exportType( + 'type', + ['jpeg', 'jpg', 'png', 'pdf', 'svg'], + ['json', 'txt'] + ); + + // export.constr + tests.exportConstr( + 'constr', + ['chart', 'stockChart', 'mapChart', 'ganttChart'], + ['stock', 'map', 'gantt'] + ); + + // export.b64 + tests.exportB64('b64'); + + // export.noDownload + tests.exportNoDownload('noDownload'); + + // export.defaultHeight + tests.exportDefaultHeight('defaultHeight'); + + // export.defaultWidth + tests.exportDefaultWidth('defaultWidth'); + + // export.defaultScale + tests.exportDefaultScale('defaultScale'); + + // export.height + tests.exportHeight('height'); + + // export.width + tests.exportWidth('width'); + + // export.scale + tests.exportScale('scale'); + + // export.globalOptions + tests.exportGlobalOptions('globalOptions'); + + // export.themeOptions + tests.exportThemeOptions('themeOptions'); + + // export.batch + tests.exportBatch('batch'); + + // export.rasterizationTimeout + tests.exportRasterizationTimeout('rasterizationTimeout'); +}); + +describe('Custom Logic configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(StrictConfigSchema.shape.customLogic, true); + + // customLogic.allowCodeExecution + tests.customLogicAllowCodeExecution('allowCodeExecution'); + + // customLogic.allowFileResources + tests.customLogicAllowFileResources('allowFileResources'); + + // customLogic.customCode + tests.customLogicCustomCode('customCode'); + + // customLogic.callback + tests.customLogicCallback('callback'); + + // customLogic.resources + tests.customLogicResources('resources'); + + // customLogic.loadConfig + tests.customLogicLoadConfig('loadConfig'); + + // customLogic.createConfig + tests.customLogicCreateConfig('createConfig'); +}); + +describe('Server configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(StrictConfigSchema.shape.server, true); + + // server.enable + tests.serverEnable('enable'); + + // server.host + tests.serverHost('host'); + + // server.port + tests.serverPort('port'); + + // server.benchmarking + tests.serverBenchmarking('benchmarking'); + + // server.proxy + tests.serverProxy('proxy', { + host: null, + port: null, + timeout: 5000 + }); + + // server.rateLimiting + tests.serverRateLimiting('rateLimiting', { + enable: false, + maxRequests: 10, + window: 1, + delay: 0, + trustProxy: false, + skipKey: null, + skipToken: null + }); + + // server.ssl + tests.serverSsl('ssl', { + enable: false, + force: false, + port: 443, + certPath: null + }); +}); + +describe('Server Proxy configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(StrictConfigSchema.shape.server.shape.proxy, true); + + // server.proxy.host + tests.serverProxyHost('host'); + + // server.proxy.port + tests.serverProxyPort('port'); + + // server.proxy.timeout + tests.serverProxyTimeout('timeout'); +}); + +describe('Server Rate Limiting configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests( + StrictConfigSchema.shape.server.shape.rateLimiting, + true + ); + + // server.rateLimiting.enable + tests.serverRateLimitingEnable('enable'); + + // server.rateLimiting.maxRequests + tests.serverRateLimitingMaxRequests('maxRequests'); + + // server.rateLimiting.window + tests.serverRateLimitingWindow('window'); + + // server.rateLimiting.delay + tests.serverRateLimitingDelay('delay'); + + // server.rateLimiting.trustProxy + tests.serverRateLimitingTrustProxy('trustProxy'); + + // server.rateLimiting.skipKey + tests.serverRateLimitingSkipKey('skipKey'); + + // server.rateLimiting.skipToken + tests.serverRateLimitingSkipToken('skipToken'); +}); + +describe('Server SSL configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(StrictConfigSchema.shape.server.shape.ssl, true); + + // server.ssl.enable + tests.serverSslEnable('enable'); + + // server.ssl.force + tests.serverSslForce('force'); + + // server.ssl.port + tests.serverSslPort('port'); + + // server.ssl.certPath + tests.serverSslCertPath('certPath'); +}); + +describe('Pool configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(StrictConfigSchema.shape.pool, true); + + // pool.minWorkers + tests.poolMinWorkers('minWorkers'); + + // pool.maxWorkers + tests.poolMaxWorkers('maxWorkers'); + + // pool.workLimit + tests.poolWorkLimit('workLimit'); + + // pool.acquireTimeout + tests.poolAcquireTimeout('acquireTimeout'); + + // pool.createTimeout + tests.poolCreateTimeout('createTimeout'); + + // pool.destroyTimeout + tests.poolDestroyTimeout('destroyTimeout'); + + // pool.idleTimeout + tests.poolIdleTimeout('idleTimeout'); + + // pool.createRetryInterval + tests.poolCreateRetryInterval('createRetryInterval'); + + // pool.reaperInterval + tests.poolReaperInterval('reaperInterval'); + + // pool.benchmarking + tests.poolBenchmarking('benchmarking'); +}); + +describe('Logging configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(StrictConfigSchema.shape.logging, true); + + // logging.level + tests.loggingLevel('level'); + + // logging.file + tests.loggingFile('file'); + + // logging.dest + tests.loggingDest('dest'); + + // logging.toConsole + tests.loggingToConsole('toConsole'); + + // logging.toFile + tests.loggingToFile('toFile'); +}); + +describe('UI configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(StrictConfigSchema.shape.ui, true); + + // ui.enable + tests.uiEnable('enable'); + + // ui.route + tests.uiRoute('route', ['/', '/ui'], ['ui', 'example/ui/']); +}); + +describe('Other configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(StrictConfigSchema.shape.other, true); + + // other.nodeEnv + tests.otherNodeEnv( + 'nodeEnv', + ['development', 'production', 'test'], + ['dev-env', 'prod-env', 'test-env'] + ); + + // other.listenToProcessExits + tests.otherListenToProcessExits('listenToProcessExits'); + + // other.noLogo + tests.otherNoLogo('noLogo'); + + // other.hardResetPage + tests.otherHardResetPage('hardResetPage'); + + // other.browserShellMode + tests.otherBrowserShellMode('browserShellMode'); + + // other.validation + tests.otherValidation('validation'); +}); + +describe('Debug configuration options should be correctly parsed and validated', () => { + // Return config tests with a specific schema and strictCheck flag injected + const tests = configTests(StrictConfigSchema.shape.debug, true); + + // debug.enable + tests.debugEnable('enable'); + + // debug.headless + tests.debugHeadless('headless'); + + // debug.devtools + tests.debugDevtools('devtools'); + + // debug.listenToConsole + tests.debugListenToConsole('listenToConsole'); + + // debug.dumpio + tests.debugDumpio('dumpio'); + + // debug.slowMo + tests.debugSlowMo('slowMo'); + + // debug.debuggingPort + tests.debugDebuggingPort('debuggingPort'); +}); diff --git a/tests/unit/validation/envs.test.js b/tests/unit/validation/envs.test.js new file mode 100644 index 00000000..7e5cd8dc --- /dev/null +++ b/tests/unit/validation/envs.test.js @@ -0,0 +1,344 @@ +/******************************************************************************* + +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. + +*******************************************************************************/ + +import { describe } from '@jest/globals'; + +import { configTests } from './shared.js'; +import { EnvSchema } from '../../../lib/validation.js'; + +// Return config tests with a specific schema and strictCheck flag injected +const tests = configTests(EnvSchema.partial(), false); + +describe('PUPPETEER environment variables should be correctly parsed and validated', () => { + // PUPPETEER_ARGS + tests.puppeteerArgs( + 'PUPPETEER_ARGS', + '--disable-sync; --enable-unsafe-webgpu; --hide-crash-restore-bubble; --hide-scrollbars; --metrics-recording-only', + [ + '--disable-sync', + '--enable-unsafe-webgpu', + '--hide-crash-restore-bubble', + '--hide-scrollbars', + '--metrics-recording-only' + ] + ); +}); + +describe('HIGHCHARTS environment variables should be correctly parsed and validated', () => { + // HIGHCHARTS_VERSION + tests.highchartsVersion('HIGHCHARTS_VERSION'); + + // HIGHCHARTS_CDN_URL + tests.highchartsCdnUrl( + 'HIGHCHARTS_CDN_URL', + ['http://example.com', 'https://example.com'], + ['http:a.com', 'http:/b.com', 'https:c.com', 'https:/d.com'] + ); + + // HIGHCHARTS_FORCE_FETCH + tests.highchartsForceFetch('HIGHCHARTS_FORCE_FETCH'); + + // HIGHCHARTS_CACHE_PATH + tests.highchartsCachePath('HIGHCHARTS_CACHE_PATH'); + + // HIGHCHARTS_ADMIN_TOKEN + tests.highchartsAdminToken('HIGHCHARTS_ADMIN_TOKEN'); + + // HIGHCHARTS_CORE_SCRIPTS + tests.highchartsCoreScripts( + 'HIGHCHARTS_CORE_SCRIPTS', + 'highcharts, highcharts-more, text1, highcharts-3d, text2', + ['highcharts', 'highcharts-more', 'highcharts-3d'] + ); + + // HIGHCHARTS_MODULE_SCRIPTS + tests.highchartsModuleScripts( + 'HIGHCHARTS_MODULE_SCRIPTS', + 'data, text1, data-tools, text2', + ['data', 'data-tools'] + ); + + // HIGHCHARTS_INDICATOR_SCRIPTS + tests.highchartsIndicatorScripts( + 'HIGHCHARTS_INDICATOR_SCRIPTS', + 'text1, indicators-all, text2', + ['indicators-all'] + ); + + // HIGHCHARTS_CUSTOM_SCRIPTS + tests.highchartsCustomScripts( + 'HIGHCHARTS_CUSTOM_SCRIPTS', + 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js, text1, https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.45/moment-timezone-with-data.min.js, text2', + [ + 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js', + 'https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.45/moment-timezone-with-data.min.js' + ] + ); +}); + +describe('EXPORT environment variables should be correctly parsed and validated', () => { + // EXPORT_INFILE + tests.exportInfile('EXPORT_INFILE'); + + // EXPORT_INSTR + tests.exportInstr('EXPORT_INSTR'); + + // EXPORT_OPTIONS + tests.exportOptions('EXPORT_OPTIONS'); + + // EXPORT_SVG + tests.exportSvg('EXPORT_SVG'); + + // EXPORT_OUTFILE + tests.exportOutfile('EXPORT_OUTFILE'); + + // EXPORT_TYPE + tests.exportType( + 'EXPORT_TYPE', + ['jpeg', 'jpg', 'png', 'pdf', 'svg'], + ['json', 'txt'] + ); + + // EXPORT_CONSTR + tests.exportConstr( + 'EXPORT_CONSTR', + ['chart', 'stockChart', 'mapChart', 'ganttChart'], + ['stock', 'map', 'gantt'] + ); + + // EXPORT_B64 + tests.exportB64('EXPORT_B64'); + + // EXPORT_NO_DOWNLOAD + tests.exportNoDownload('EXPORT_NO_DOWNLOAD'); + + // EXPORT_DEFAULT_HEIGHT + tests.exportDefaultHeight('EXPORT_DEFAULT_HEIGHT'); + + // EXPORT_DEFAULT_WIDTH + tests.exportDefaultWidth('EXPORT_DEFAULT_WIDTH'); + + // EXPORT_DEFAULT_SCALE + tests.exportDefaultScale('EXPORT_DEFAULT_SCALE'); + + // EXPORT_HEIGHT + tests.exportDefaultHeight('EXPORT_HEIGHT'); + + // EXPORT_WIDTH + tests.exportDefaultWidth('EXPORT_WIDTH'); + + // EXPORT_SCALE + tests.exportDefaultScale('EXPORT_SCALE'); + + // EXPORT_GLOBAL_OPTIONS + tests.exportGlobalOptions('EXPORT_GLOBAL_OPTIONS'); + + // EXPORT_THEME_OPTIONS + tests.exportThemeOptions('EXPORT_THEME_OPTIONS'); + + // EXPORT_BATCH + tests.exportBatch('EXPORT_BATCH'); + + // EXPORT_RASTERIZATION_TIMEOUT + tests.exportRasterizationTimeout('EXPORT_RASTERIZATION_TIMEOUT'); +}); + +describe('CUSTOM_LOGIC environment variables should be correctly parsed and validated', () => { + // CUSTOM_LOGIC_ALLOW_CODE_EXECUTION + tests.customLogicAllowCodeExecution('CUSTOM_LOGIC_ALLOW_CODE_EXECUTION'); + + // CUSTOM_LOGIC_ALLOW_FILE_RESOURCES + tests.customLogicAllowFileResources('CUSTOM_LOGIC_ALLOW_FILE_RESOURCES'); + + // CUSTOM_LOGIC_CUSTOM_CODE + tests.customLogicCustomCode('CUSTOM_LOGIC_CUSTOM_CODE'); + + // CUSTOM_LOGIC_CALLBACK + tests.customLogicCallback('CUSTOM_LOGIC_CALLBACK'); + + // CUSTOM_LOGIC_RESOURCES + tests.customLogicResources('CUSTOM_LOGIC_RESOURCES'); + + // CUSTOM_LOGIC_LOAD_CONFIG + tests.customLogicLoadConfig('CUSTOM_LOGIC_LOAD_CONFIG'); + + // CUSTOM_LOGIC_CREATE_CONFIG + tests.customLogicCreateConfig('CUSTOM_LOGIC_CREATE_CONFIG'); +}); + +describe('SERVER environment variables should be correctly parsed and validated', () => { + // SERVER_ENABLE + tests.serverEnable('SERVER_ENABLE'); + + // SERVER_HOST + tests.serverHost('SERVER_HOST'); + + // SERVER_PORT + tests.serverPort('SERVER_PORT'); + + // SERVER_BENCHMARKING + tests.serverBenchmarking('SERVER_BENCHMARKING'); +}); + +describe('SERVER_PROXY environment variables should be correctly parsed and validated', () => { + // SERVER_PROXY_HOST + tests.serverProxyHost('SERVER_PROXY_HOST'); + + // SERVER_PROXY_PORT + tests.serverProxyPort('SERVER_PROXY_PORT'); + + // SERVER_PROXY_TIMEOUT + tests.serverProxyTimeout('SERVER_PROXY_TIMEOUT'); +}); + +describe('SERVER_RATE_LIMITING environment variables should be correctly parsed and validated', () => { + // SERVER_RATE_LIMITING_ENABLE + tests.serverRateLimitingEnable('SERVER_RATE_LIMITING_ENABLE'); + + // SERVER_RATE_LIMITING_MAX_REQUESTS + tests.serverRateLimitingMaxRequests('SERVER_RATE_LIMITING_MAX_REQUESTS'); + + // SERVER_RATE_LIMITING_WINDOW + tests.serverRateLimitingWindow('SERVER_RATE_LIMITING_WINDOW'); + + // SERVER_RATE_LIMITING_DELAY + tests.serverRateLimitingDelay('SERVER_RATE_LIMITING_DELAY'); + + // SERVER_RATE_LIMITING_TRUST_PROXY + tests.serverRateLimitingTrustProxy('SERVER_RATE_LIMITING_TRUST_PROXY'); + + // SERVER_RATE_LIMITING_SKIP_KEY + tests.serverRateLimitingSkipKey('SERVER_RATE_LIMITING_SKIP_KEY'); + + // SERVER_RATE_LIMITING_SKIP_TOKEN + tests.serverRateLimitingSkipToken('SERVER_RATE_LIMITING_SKIP_TOKEN'); +}); + +describe('SERVER_SSL environment variables should be correctly parsed and validated', () => { + // SERVER_SSL_ENABLE + tests.serverSslEnable('SERVER_SSL_ENABLE'); + + // SERVER_SSL_FORCE + tests.serverSslForce('SERVER_SSL_FORCE'); + + // SERVER_SSL_PORT + tests.serverSslPort('SERVER_SSL_PORT'); + + // SERVER_SSL_CERT_PATH + tests.serverSslCertPath('SERVER_SSL_CERT_PATH'); +}); + +describe('POOL environment variables should be correctly parsed and validated', () => { + // POOL_MIN_WORKERS + tests.poolMinWorkers('POOL_MIN_WORKERS'); + + // POOL_MAX_WORKERS + tests.poolMaxWorkers('POOL_MAX_WORKERS'); + + // POOL_WORK_LIMIT + tests.poolWorkLimit('POOL_WORK_LIMIT'); + + // POOL_ACQUIRE_TIMEOUT + tests.poolAcquireTimeout('POOL_ACQUIRE_TIMEOUT'); + + // POOL_CREATE_TIMEOUT + tests.poolCreateTimeout('POOL_CREATE_TIMEOUT'); + + // POOL_DESTROY_TIMEOUT + tests.poolDestroyTimeout('POOL_DESTROY_TIMEOUT'); + + // POOL_IDLE_TIMEOUT + tests.poolIdleTimeout('POOL_IDLE_TIMEOUT'); + + // POOL_CREATE_RETRY_INTERVAL + tests.poolCreateRetryInterval('POOL_CREATE_RETRY_INTERVAL'); + + // POOL_REAPER_INTERVAL + tests.poolReaperInterval('POOL_REAPER_INTERVAL'); + + // POOL_BENCHMARKING + tests.poolBenchmarking('POOL_BENCHMARKING'); +}); + +describe('LOGGING environment variables should be correctly parsed and validated', () => { + // LOGGING_LEVEL + tests.loggingLevel('LOGGING_LEVEL'); + + // LOGGING_FILE + tests.loggingFile('LOGGING_FILE'); + + // LOGGING_DEST + tests.loggingDest('LOGGING_DEST'); + + // LOGGING_TO_CONSOLE + tests.loggingToConsole('LOGGING_TO_CONSOLE'); + + // LOGGING_TO_FILE + tests.loggingToFile('LOGGING_TO_FILE'); +}); + +describe('UI environment variables should be correctly parsed and validated', () => { + // UI_ENABLE + tests.uiEnable('UI_ENABLE'); + + // UI_ROUTE + tests.uiRoute('UI_ROUTE', ['/', '/ui'], ['ui', 'example/ui/']); +}); + +describe('OTHER environment variables should be correctly parsed and validated', () => { + // OTHER_NODE_ENV + tests.otherNodeEnv( + 'OTHER_NODE_ENV', + ['development', 'production', 'test'], + ['dev-env', 'prod-env', 'test-env'] + ); + + // OTHER_LISTEN_TO_PROCESS_EXITS + tests.otherListenToProcessExits('OTHER_LISTEN_TO_PROCESS_EXITS'); + + // OTHER_NO_LOGO + tests.otherNoLogo('OTHER_NO_LOGO'); + + // OTHER_HARD_RESET_PAGE + tests.otherHardResetPage('OTHER_HARD_RESET_PAGE'); + + // OTHER_BROWSER_SHELL_MODE + tests.otherBrowserShellMode('OTHER_BROWSER_SHELL_MODE'); + + // OTHER_VALIDATION + tests.otherValidation('OTHER_VALIDATION'); +}); + +describe('DEBUG environment variables should be correctly parsed and validated', () => { + // DEBUG_ENABLE + tests.debugEnable('DEBUG_ENABLE'); + + // DEBUG_HEADLESS + tests.debugHeadless('DEBUG_HEADLESS'); + + // DEBUG_DEVTOOLS + tests.debugDevtools('DEBUG_DEVTOOLS'); + + // DEBUG_LISTEN_TO_CONSOLE + tests.debugListenToConsole('DEBUG_LISTEN_TO_CONSOLE'); + + // DEBUG_DUMPIO + tests.debugDumpio('DEBUG_DUMPIO'); + + // DEBUG_SLOW_MO + tests.debugSlowMo('DEBUG_SLOW_MO'); + + // DEBUG_DEBUGGING_PORT + tests.debugDebuggingPort('DEBUG_DEBUGGING_PORT'); +}); diff --git a/tests/unit/validation/shared.js b/tests/unit/validation/shared.js new file mode 100644 index 00000000..4edc16ab --- /dev/null +++ b/tests/unit/validation/shared.js @@ -0,0 +1,2559 @@ +/******************************************************************************* + +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. + +*******************************************************************************/ + +import { describe, expect, it } from '@jest/globals'; + +import { validatePropOfSchema } from '../../utils/testUtils.js'; + +/** + * Runs a series of tests to validate and parse configuration properties using + * the injected schema. Optionally performs strict checks. + * + * @param {Object} schema - The schema used for validation and parsing. + * @param {boolean} strictCheck - A flag indicating whether to enforce strict + * validation. + */ +export function configTests(schema, strictCheck) { + /** + * Verifies that a property with the value undefined is accepted. + * + * @function acceptUndefined + * + * @param {string} property - The property to check for accepting null. + * + * @throws {Error} Throws an `Error` if the schema validation fails. + */ + const acceptUndefined = (property) => { + const obj = { [property]: undefined }; + expect(schema.parse(obj)[property]).toBe(undefined); + }; + + /** + * Verifies that a property with the value null is accepted. + * + * @function acceptNull + * + * @param {string} property - The property to check for accepting null. + * + * @throws {Error} Throws an `Error` if the schema validation fails. + */ + const acceptNull = (property) => { + const obj = { [property]: null }; + expect(schema.parse(obj)[property]).toBe(null); + }; + + /** + * Verifies that a property with the string value 'null' is converted to null. + * + * @function stringNullToNull + * + * @param {string} property - The property to check for conversion of 'null' + * to null. + * + * @throws {Error} Throws an `Error` if the schema validation fails. + */ + const stringNullToNull = (property) => { + const obj = { [property]: 'null' }; + expect(schema.parse(obj)[property]).toBe(null); + }; + + /** + * Verifies that a property with the string value 'undefined' is converted to + * null. + * + * @function stringUndefinedToNull + * + * @param {string} property - The property to check for conversion of + * 'undefined' to null. + * + * @throws {Error} Throws an `Error` if the schema validation fails. + */ + const stringUndefinedToNull = (property) => { + const obj = { [property]: 'undefined' }; + expect(schema.parse(obj)[property]).toBe(null); + }; + + /** + * Verifies that a property with the string value '' is converted to null. + * + * @function emptyStringToNull + * + * @param {string} property - The property to check for conversion of '' to + * null. + * + * @throws {Error} Throws an `Error` if the schema validation fails. + */ + const emptyStringToNull = (property) => { + const obj = { [property]: '' }; + expect(schema.parse(obj)[property]).toBe(null); + }; + + /** + * Verifies that a property set to null causes a schema validation error. + * + * @function nullThrow + * + * @param {string} property - The property to check for a thrown validation + * error. + * + * @throws {Error} Throws an `Error` if the schema validation fails. + */ + const nullThrow = (property) => { + const obj = { [property]: null }; + expect(() => schema.parse(obj)).toThrow(); + }; + + /** + * Verifies that a property set to 'null' causes a schema validation error. + * + * @function stringNullThrow + * + * @param {string} property - The property to check for a thrown validation + * error. + * + * @throws {Error} Throws an `Error` if the schema validation fails. + */ + const stringNullThrow = (property) => { + const obj = { [property]: 'null' }; + expect(() => schema.parse(obj)).toThrow(); + }; + + /** + * Verifies that a property set to 'undefined' causes a schema validation + * error. + * + * @function stringUndefinedThrow + * + * @param {string} property - The property to check for a thrown validation + * error. + * + * @throws {Error} Throws an `Error` if the schema validation fails. + */ + const stringUndefinedThrow = (property) => { + const obj = { [property]: 'undefined' }; + expect(() => schema.parse(obj)).toThrow(); + }; + + /** + * Verifies that a property set to '' causes a schema validation error. + * + * @function emptyStringThrow + * + * @param {string} property - The property to check for a thrown validation + * error. + * + * @throws {Error} Throws an `Error` if the schema validation fails. + */ + const emptyStringThrow = (property) => { + const obj = { [property]: '' }; + expect(() => schema.parse(obj)).toThrow(); + }; + + /** + * Object that contains all tests for validating and parsing values of the + * options config. + */ + const validationTests = { + /** + * The boolean validator. + */ + boolean(property) { + it('should accept a boolean value', () => { + const obj = { [property]: true }; + expect(schema.parse(obj)[property]).toBe(true); + obj[property] = false; + expect(schema.parse(obj)[property]).toBe(false); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + if (strictCheck) { + it('should not accept a stringified boolean value', () => { + const obj = { [property]: 'true' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = 'false'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept null', () => { + nullThrow(property); + }); + + it('should not accept a stringified undefined', () => { + stringUndefinedThrow(property); + }); + + it('should not accept a stringified null', () => { + stringNullThrow(property); + }); + + it('should not accept an empty string', () => { + emptyStringThrow(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, ['boolean', 'undefined'])); + } else { + it('should accept a stringified boolean value and transform it to a boolean', () => { + const obj = { [property]: 'true' }; + expect(schema.parse(obj)[property]).toBe(true); + obj[property] = 'false'; + expect(schema.parse(obj)[property]).toBe(false); + }); + + it('should accept a stringified 0 and 1 values and transform them to a boolean', () => { + const obj = { [property]: '1' }; + expect(schema.parse(obj)[property]).toBe(true); + obj[property] = '0'; + expect(schema.parse(obj)[property]).toBe(false); + }); + + it('should accept null', () => { + acceptNull(property); + }); + + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'stringBoolean', + 'stringNumber', + 'stringUndefined', + 'stringNull', + 'boolean', + 'undefined', + 'null' + ])); + } + }, + + /** + * The string validator. + */ + string(property, strictCheck) { + it('should accept a string value', () => { + const obj = { [property]: 'text' }; + expect(schema.parse(obj)[property]).toBe('text'); + obj[property] = 'some-other-text'; + expect(schema.parse(obj)[property]).toBe('some-other-text'); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + if (strictCheck) { + it("should not accept 'false', 'undefined', 'null', '' values", () => { + const obj = { [property]: 'false' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = 'undefined'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = 'null'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = ''; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept null', () => { + nullThrow(property); + }); + + it('should not accept a stringified undefined', () => { + stringUndefinedThrow(property); + }); + + it('should not accept a stringified null', () => { + stringNullThrow(property); + }); + + it('should not accept an empty string', () => { + emptyStringThrow(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'string', + 'stringBoolean', + 'stringNumber', + 'stringBigInt', + 'stringSymbol', + 'stringObject', + 'stringArray', + 'stringFunction', + 'stringOther', + 'undefined' + ])); + } else { + it("should accept 'false', 'undefined', 'null', '' values and trasform to null", () => { + const obj = { [property]: 'false' }; + expect(schema.parse(obj)[property]).toBe(null); + obj[property] = 'undefined'; + expect(schema.parse(obj)[property]).toBe(null); + obj[property] = 'null'; + expect(schema.parse(obj)[property]).toBe(null); + obj[property] = ''; + expect(schema.parse(obj)[property]).toBe(null); + }); + + it('should accept null', () => { + acceptNull(property); + }); + + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'string', + 'stringBoolean', + 'stringNumber', + 'stringBigInt', + 'stringUndefined', + 'stringNull', + 'stringSymbol', + 'stringObject', + 'stringArray', + 'stringFunction', + 'stringOther', + 'undefined', + 'null' + ])); + } + }, + + /** + * The accept values validator. + */ + acceptValues(property, correctValue, incorrectValue) { + it(`should accept the following ${correctValue.join(', ')} values`, () => { + correctValue.forEach((value) => { + expect(schema.parse({ [property]: value })[property]).toBe(value); + }); + }); + + it(`should not accept the following ${incorrectValue.join(', ')} values`, () => { + incorrectValue.forEach((value) => { + expect(() => schema.parse({ [property]: value })).toThrow(); + }); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + if (strictCheck) { + it('should not accept null', () => { + nullThrow(property); + }); + + it('should not accept a stringified undefined', () => { + stringUndefinedThrow(property); + }); + + it('should not accept a stringified null', () => { + stringNullThrow(property); + }); + + it('should not accept an empty string', () => { + emptyStringThrow(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, ['undefined'])); + } else { + it('should accept null', () => { + acceptNull(property); + }); + + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'stringUndefined', + 'stringNull', + 'undefined', + 'null' + ])); + } + }, + + /** + * The nullable accept values validator. + */ + nullableAcceptValues(property, correctValue, incorrectValue) { + it(`should accept the following ${correctValue.join(', ')} values`, () => { + correctValue.forEach((value) => { + expect(schema.parse({ [property]: value })[property]).toBe(value); + }); + }); + + it(`should not accept the following ${incorrectValue.join(', ')} values`, () => { + incorrectValue.forEach((value) => { + expect(() => schema.parse({ [property]: value })).toThrow(); + }); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + it('should accept null', () => { + acceptNull(property); + }); + + if (strictCheck) { + it('should not accept a stringified undefined', () => { + stringUndefinedThrow(property); + }); + + it('should not accept a stringified null', () => { + stringNullThrow(property); + }); + + it('should not accept an empty string', () => { + emptyStringThrow(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, ['undefined', 'null'])); + } else { + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'stringUndefined', + 'stringNull', + 'undefined', + 'null' + ])); + } + }, + + /** + * The array of strings validator. + */ + stringArray(property, value, correctValue, separator = ',') { + it('should accept a string value or an array of strings and correctly parse it to an array of strings', () => { + const obj = { [property]: value }; + expect(schema.parse(obj)[property]).toEqual(correctValue); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + if (strictCheck) { + it('should accept an empty array', () => { + const obj = { [property]: [] }; + expect(schema.parse(obj)[property]).toEqual([]); + }); + + it('should accept an array of strings and filter it from the forbidden values', () => { + const obj = { + [property]: [...value, 'false', 'undefined', 'null', ''] + }; + expect(schema.parse(obj)[property]).toEqual(correctValue); + }); + + it('should not accept null', () => { + nullThrow(property); + }); + + it('should not accept a stringified undefined', () => { + stringUndefinedThrow(property); + }); + + it('should not accept a stringified null', () => { + stringNullThrow(property); + }); + + it('should not accept an empty string', () => { + emptyStringThrow(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, ['undefined', 'array'])); + } else { + it("should accept a stringified array of the 'values' string and correctly parse it to an array of strings", () => { + const obj = { [property]: `[${value}]` }; + expect(schema.parse(obj)[property]).toEqual(correctValue); + }); + + it('should filter a stringified array of a values string from forbidden values and correctly parse it to an array of strings', () => { + const obj = { + [property]: `[${value}${separator} false${separator} undefined${separator} null${separator}]` + }; + expect(schema.parse(obj)[property]).toEqual(correctValue); + }); + + it('should accept null', () => { + acceptNull(property); + }); + + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'string', + 'stringBoolean', + 'stringNumber', + 'stringBigInt', + 'stringUndefined', + 'stringNull', + 'stringSymbol', + 'stringObject', + 'stringArray', + 'stringFunction', + 'stringOther', + 'array', + 'undefined', + 'null' + ])); + } + }, + + /** + * The positive number validator. + */ + positiveNum(property) { + it('should accept a positive number value', () => { + const obj = { [property]: 0.1 }; + expect(schema.parse(obj)[property]).toBe(0.1); + obj[property] = 100.5; + expect(schema.parse(obj)[property]).toBe(100.5); + obj[property] = 750; + expect(schema.parse(obj)[property]).toBe(750); + }); + + it('should not accept negative and non-positive number value', () => { + const obj = { [property]: 0 }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = -100; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept stringified negative and non-positive number value', () => { + const obj = { [property]: '0' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '-100'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + if (strictCheck) { + it('should not accept a stringified positive number value', () => { + const obj = { [property]: '0.1' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '100.5'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '750'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept null', () => { + nullThrow(property); + }); + + it('should not accept a stringified undefined', () => { + stringUndefinedThrow(property); + }); + + it('should not accept a stringified null', () => { + stringNullThrow(property); + }); + + it('should not accept an empty string', () => { + emptyStringThrow(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'stringNumber', + 'stringUndefined', + 'stringNull', + 'number', + 'undefined', + 'null' + ])); + } else { + it('should accept a stringified positive number value', () => { + const obj = { [property]: '0.1' }; + expect(schema.parse(obj)[property]).toBe(0.1); + obj[property] = '100.5'; + expect(schema.parse(obj)[property]).toBe(100.5); + obj[property] = '750'; + expect(schema.parse(obj)[property]).toBe(750); + }); + + it('should accept null', () => { + acceptNull(property); + }); + + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'stringNumber', + 'stringUndefined', + 'stringNull', + 'number', + 'undefined', + 'null' + ])); + } + }, + + /** + * The nullable positive number validator. + */ + nullablePositiveNum(property) { + it('should accept a positive number value', () => { + const obj = { [property]: 0.1 }; + expect(schema.parse(obj)[property]).toBe(0.1); + obj[property] = 100.5; + expect(schema.parse(obj)[property]).toBe(100.5); + obj[property] = 750; + expect(schema.parse(obj)[property]).toBe(750); + }); + + it('should not accept negative and non-positive number value', () => { + const obj = { [property]: 0 }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = -100; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept stringified negative and non-positive number value', () => { + const obj = { [property]: '0' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '-100'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + it('should accept null', () => { + acceptNull(property); + }); + + if (strictCheck) { + it('should not accept a stringified positive number value', () => { + const obj = { [property]: '0.1' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '100.5'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '750'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept a stringified undefined', () => { + stringUndefinedThrow(property); + }); + + it('should not accept a stringified null', () => { + stringNullThrow(property); + }); + + it('should not accept an empty string', () => { + emptyStringThrow(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'stringNumber', + 'stringUndefined', + 'stringNull', + 'number', + 'undefined', + 'null' + ])); + } else { + it('should accept a stringified positive number value', () => { + const obj = { [property]: '0.1' }; + expect(schema.parse(obj)[property]).toBe(0.1); + obj[property] = '100.5'; + expect(schema.parse(obj)[property]).toBe(100.5); + obj[property] = '750'; + expect(schema.parse(obj)[property]).toBe(750); + }); + + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'stringNumber', + 'stringUndefined', + 'stringNull', + 'number', + 'undefined', + 'null' + ])); + } + }, + + /** + * The non-negative number validator. + */ + nonNegativeNum(property) { + it('should accept a non-negative number value', () => { + const obj = { [property]: 0 }; + expect(schema.parse(obj)[property]).toBe(0); + obj[property] = 1000; + expect(schema.parse(obj)[property]).toBe(1000); + }); + + it('should not accept a negative number value', () => { + const obj = { [property]: -1000 }; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept a stringified negative number value', () => { + const obj = { [property]: '-1000' }; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + if (strictCheck) { + it('should not accept a stringified non-negative number value', () => { + const obj = { [property]: '0' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '1000'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept null', () => { + nullThrow(property); + }); + + it('should not accept a stringified undefined', () => { + stringUndefinedThrow(property); + }); + + it('should not accept a stringified null', () => { + stringNullThrow(property); + }); + + it('should not accept an empty string', () => { + emptyStringThrow(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'stringNumber', + 'stringUndefined', + 'stringNull', + 'number', + 'undefined', + 'null' + ])); + } else { + it('should accept a stringified non-negative number value', () => { + const obj = { [property]: '0' }; + expect(schema.parse(obj)[property]).toBe(0); + obj[property] = '1000'; + expect(schema.parse(obj)[property]).toBe(1000); + }); + + it('should accept null', () => { + acceptNull(property); + }); + + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'stringNumber', + 'stringUndefined', + 'stringNull', + 'number', + 'undefined', + 'null' + ])); + } + }, + + /** + * The nullable non-negative number validator. + */ + nullableNonNegativeNum(property) { + it('should accept a non-negative number value', () => { + const obj = { [property]: 0 }; + expect(schema.parse(obj)[property]).toBe(0); + obj[property] = 1000; + expect(schema.parse(obj)[property]).toBe(1000); + }); + + it('should not accept a negative number value', () => { + const obj = { [property]: -1000 }; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept a stringified negative number value', () => { + const obj = { [property]: '-1000' }; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + it('should accept null', () => { + acceptNull(property); + }); + + if (strictCheck) { + it('should not accept a stringified non-negative number value', () => { + const obj = { [property]: '0' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '1000'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept a stringified undefined', () => { + stringUndefinedThrow(property); + }); + + it('should not accept a stringified null', () => { + stringNullThrow(property); + }); + + it('should not accept an empty string', () => { + emptyStringThrow(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'stringNumber', + 'stringUndefined', + 'stringNull', + 'number', + 'undefined', + 'null' + ])); + } else { + it('should accept a stringified non-negative number value', () => { + const obj = { [property]: '0' }; + expect(schema.parse(obj)[property]).toBe(0); + obj[property] = '1000'; + expect(schema.parse(obj)[property]).toBe(1000); + }); + + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'stringNumber', + 'stringUndefined', + 'stringNull', + 'number', + 'undefined', + 'null' + ])); + } + }, + + /** + * The svg validator. + */ + svg(property) { + it('should accept a string value that starts with the { + const obj = { + [property]: "..." + }; + expect(schema.parse(obj)[property]).toBe( + "..." + ); + obj[property] = + '...'; + expect(schema.parse(obj)[property]).toBe( + '...' + ); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + it("should accept 'false', 'undefined', 'null', '' values and trasform to null", () => { + const obj = { [property]: 'false' }; + expect(schema.parse(obj)[property]).toBe(null); + obj[property] = 'undefined'; + expect(schema.parse(obj)[property]).toBe(null); + obj[property] = 'null'; + expect(schema.parse(obj)[property]).toBe(null); + obj[property] = ''; + expect(schema.parse(obj)[property]).toBe(null); + }); + + it('should accept null', () => { + acceptNull(property); + }); + + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'string', + 'stringBoolean', + 'stringNumber', + 'stringBigInt', + 'stringUndefined', + 'stringNull', + 'stringSymbol', + 'stringObject', + 'stringArray', + 'stringFunction', + 'stringOther', + 'undefined', + 'null' + ])); + }, + + /** + * The chartConfig validator. + */ + chartConfig(property) { + it('should accept any object values', () => { + const obj = { [property]: {} }; + expect(schema.parse(obj)[property]).toEqual({}); + obj[property] = { a: 1 }; + expect(schema.parse(obj)[property]).toEqual({ a: 1 }); + obj[property] = { a: '1', b: { c: 3 } }; + expect(schema.parse(obj)[property]).toEqual({ a: '1', b: { c: 3 } }); + }); + + it("should accept a string value that starts with the '{' and ends with the '}'", () => { + const obj = { [property]: '{}' }; + expect(schema.parse(obj)[property]).toBe('{}'); + obj[property] = '{ a: 1 }'; + expect(schema.parse(obj)[property]).toBe('{ a: 1 }'); + obj[property] = '{ a: "1", b: { c: 3 } }'; + expect(schema.parse(obj)[property]).toBe('{ a: "1", b: { c: 3 } }'); + }); + + it('should not accept any array values', () => { + const obj = { [property]: [] }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = [1]; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = ['a']; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = [{ a: 1 }]; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept any other object based values', () => { + const obj = { [property]: function () {} }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = () => {}; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = new Date(); + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + it('should accept null', () => { + acceptNull(property); + }); + + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'undefined', + 'null', + 'emptyString', + 'stringUndefined', + 'stringNull', + 'stringObject', + 'object', + 'other' + ])); + }, + + /** + * The additionalOptions validator. + */ + additionalOptions(property) { + it('should accept any object values', () => { + const obj = { [property]: {} }; + expect(schema.parse(obj)[property]).toEqual({}); + obj[property] = { a: 1 }; + expect(schema.parse(obj)[property]).toEqual({ a: 1 }); + obj[property] = { a: '1', b: { c: 3 } }; + expect(schema.parse(obj)[property]).toEqual({ a: '1', b: { c: 3 } }); + }); + + it("should accept a string value that starts with the '{' and ends with the '}'", () => { + const obj = { [property]: '{}' }; + expect(schema.parse(obj)[property]).toBe('{}'); + obj[property] = '{ a: 1 }'; + expect(schema.parse(obj)[property]).toBe('{ a: 1 }'); + obj[property] = '{ a: "1", b: { c: 3 } }'; + expect(schema.parse(obj)[property]).toBe('{ a: "1", b: { c: 3 } }'); + }); + + it('should accept string values that end with .json', () => { + const obj = { [property]: 'options.json' }; + expect(schema.parse(obj)[property]).toBe('options.json'); + }); + + it('should not accept string values that do not end with .json', () => { + const obj = { [property]: 'options.pdf' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = 'options.png'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept string values that are not at least one character long without the extensions', () => { + const obj = { [property]: '.json' }; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept any array values', () => { + const obj = { [property]: [] }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = [1]; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = ['a']; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = [{ a: 1 }]; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept any other object based values', () => { + const obj = { [property]: function () {} }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = () => {}; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = new Date(); + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + it('should accept null', () => { + acceptNull(property); + }); + + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'undefined', + 'null', + 'emptyString', + 'stringUndefined', + 'stringNull', + 'stringObject', + 'object', + 'other' + ])); + }, + + /** + * The infile option validator. + */ + infile(property) { + it('should accept string values that end with .json or .svg', () => { + const obj = { [property]: 'chart.json' }; + expect(schema.parse(obj)[property]).toBe('chart.json'); + obj[property] = 'chart.svg'; + expect(schema.parse(obj)[property]).toBe('chart.svg'); + }); + + it('should not accept string values that do not end with .json or .svg', () => { + const obj = { [property]: 'chart.pdf' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = 'chart.png'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept string values that are not at least one character long without the extensions', () => { + const obj = { [property]: '.json' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '.svg'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + it('should accept null', () => { + acceptNull(property); + }); + + if (strictCheck) { + it('should not accept a stringified undefined', () => { + stringUndefinedThrow(property); + }); + + it('should not accept a stringified null', () => { + stringNullThrow(property); + }); + + it('should not accept an empty string', () => { + emptyStringThrow(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, ['undefined', 'null'])); + } else { + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'stringUndefined', + 'stringNull', + 'undefined', + 'null' + ])); + } + }, + + /** + * The outfile option validator. + */ + outfile(property) { + it('should accept string values that end with .jpeg, .jpg, .png, .pdf, or .svg', () => { + const obj = { [property]: 'chart.jpeg' }; + expect(schema.parse(obj)[property]).toBe('chart.jpeg'); + obj[property] = 'chart.jpg'; + expect(schema.parse(obj)[property]).toBe('chart.jpg'); + obj[property] = 'chart.png'; + expect(schema.parse(obj)[property]).toBe('chart.png'); + obj[property] = 'chart.pdf'; + expect(schema.parse(obj)[property]).toBe('chart.pdf'); + obj[property] = 'chart.svg'; + expect(schema.parse(obj)[property]).toBe('chart.svg'); + }); + + it('should not accept string values that do not end with .jpeg, .jpg, .png, .pdf, or .svg', () => { + const obj = { [property]: 'chart.json' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = 'chart.txt'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept string values that are not at least one character long without the extensions', () => { + const obj = { [property]: '.jpeg' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '.jpg'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '.png'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '.pdf'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '.svg'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + it('should accept null', () => { + acceptNull(property); + }); + + if (strictCheck) { + it('should not accept a stringified undefined', () => { + stringUndefinedThrow(property); + }); + + it('should not accept a stringified null', () => { + stringNullThrow(property); + }); + + it('should not accept an empty string', () => { + emptyStringThrow(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, ['undefined', 'null'])); + } else { + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'stringUndefined', + 'stringNull', + 'undefined', + 'null' + ])); + } + }, + + /** + * The version option validator. + */ + version(property) { + it("should accept the 'latest' value", () => { + const obj = { [property]: 'latest' }; + expect(schema.parse(obj)[property]).toBe('latest'); + }); + + it('should accept a value in XX, XX.YY, XX.YY.ZZ formats', () => { + const obj = { [property]: '1' }; + expect(schema.parse(obj)[property]).toBe('1'); + obj[property] = '11'; + expect(schema.parse(obj)[property]).toBe('11'); + obj[property] = '1.1'; + expect(schema.parse(obj)[property]).toBe('1.1'); + obj[property] = '1.11'; + expect(schema.parse(obj)[property]).toBe('1.11'); + obj[property] = '11.1'; + expect(schema.parse(obj)[property]).toBe('11.1'); + obj[property] = '11.11'; + expect(schema.parse(obj)[property]).toBe('11.11'); + obj[property] = '1.1.1'; + expect(schema.parse(obj)[property]).toBe('1.1.1'); + obj[property] = '1.1.11'; + expect(schema.parse(obj)[property]).toBe('1.1.11'); + obj[property] = '1.11.1'; + expect(schema.parse(obj)[property]).toBe('1.11.1'); + obj[property] = '1.11.11'; + expect(schema.parse(obj)[property]).toBe('1.11.11'); + obj[property] = '11.1.1'; + expect(schema.parse(obj)[property]).toBe('11.1.1'); + obj[property] = '11.1.11'; + expect(schema.parse(obj)[property]).toBe('11.1.11'); + obj[property] = '11.11.1'; + expect(schema.parse(obj)[property]).toBe('11.11.1'); + obj[property] = '11.11.11'; + expect(schema.parse(obj)[property]).toBe('11.11.11'); + }); + + it('should not accept other string value', () => { + const obj = { [property]: 'string-other-than-latest' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '11a.2.0'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '11.2.123'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + if (strictCheck) { + it('should not accept null', () => { + nullThrow(property); + }); + + it('should not accept a stringified undefined', () => { + stringUndefinedThrow(property); + }); + + it('should not accept a stringified null', () => { + stringNullThrow(property); + }); + + it('should not accept an empty string', () => { + emptyStringThrow(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'stringNumber', + 'undefined' + ])); + } else { + it('should accept null', () => { + acceptNull(property); + }); + + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'stringNumber', + 'stringUndefined', + 'stringNull', + 'undefined', + 'null' + ])); + } + }, + + /** + * The scale option validator. + */ + scale(property) { + it('should accept number values between the 0.1 and 5.0', () => { + const obj = { [property]: 0.1 }; + expect(schema.parse(obj)[property]).toBe(0.1); + obj[property] = 1; + expect(schema.parse(obj)[property]).toBe(1); + obj[property] = 1.5; + expect(schema.parse(obj)[property]).toBe(1.5); + obj[property] = 5; + expect(schema.parse(obj)[property]).toBe(5); + }); + + it('should not accept number values outside the 0.1 and 5.0', () => { + const obj = { [property]: -1.1 }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = 0; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = 5.5; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept stringified number values outside the 0.1 and 5.0', () => { + const obj = { [property]: '-1.1' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '0'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '5.5'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + if (strictCheck) { + it('should not accept stringified number values between the 0.1 and 5.0', () => { + const obj = { [property]: '0.1' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '1'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '1.5'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '5'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept null', () => { + nullThrow(property); + }); + + it('should not accept a stringified undefined', () => { + stringUndefinedThrow(property); + }); + + it('should not accept a stringified null', () => { + stringNullThrow(property); + }); + + it('should not accept an empty string', () => { + emptyStringThrow(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, ['number', 'undefined'])); + } else { + it('should accept stringified number values between the 0.1 and 5.0', () => { + const obj = { [property]: '0.1' }; + expect(schema.parse(obj)[property]).toBe(0.1); + obj[property] = '1'; + expect(schema.parse(obj)[property]).toBe(1); + obj[property] = '1.5'; + expect(schema.parse(obj)[property]).toBe(1.5); + obj[property] = '5'; + expect(schema.parse(obj)[property]).toBe(5); + }); + + it('should accept null', () => { + acceptNull(property); + }); + + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'stringNumber', + 'stringUndefined', + 'stringNull', + 'number', + 'undefined', + 'null' + ])); + } + }, + + /** + * The nullable scale option validator. + */ + nullableScale(property) { + it('should accept number values between the 0.1 and 5.0', () => { + const obj = { [property]: 0.1 }; + expect(schema.parse(obj)[property]).toBe(0.1); + obj[property] = 1; + expect(schema.parse(obj)[property]).toBe(1); + obj[property] = 1.5; + expect(schema.parse(obj)[property]).toBe(1.5); + obj[property] = 5; + expect(schema.parse(obj)[property]).toBe(5); + }); + + it('should not accept number values outside the 0.1 and 5.0', () => { + const obj = { [property]: -1.1 }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = 0; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = 5.5; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept stringified number values outside the 0.1 and 5.0', () => { + const obj = { [property]: '-1.1' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '0'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '5.5'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + it('should accept null', () => { + acceptNull(property); + }); + + if (strictCheck) { + it('should not accept stringified number values between the 0.1 and 5.0', () => { + const obj = { [property]: '0.1' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '1'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '1.5'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '5'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept a stringified undefined', () => { + stringUndefinedThrow(property); + }); + + it('should not accept a stringified null', () => { + stringNullThrow(property); + }); + + it('should not accept an empty string', () => { + emptyStringThrow(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'number', + 'undefined', + 'null' + ])); + } else { + it('should accept stringified number values between the 0.1 and 5.0', () => { + const obj = { [property]: '0.1' }; + expect(schema.parse(obj)[property]).toBe(0.1); + obj[property] = '1'; + expect(schema.parse(obj)[property]).toBe(1); + obj[property] = '1.5'; + expect(schema.parse(obj)[property]).toBe(1.5); + obj[property] = '5'; + expect(schema.parse(obj)[property]).toBe(5); + }); + + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'stringNumber', + 'stringUndefined', + 'stringNull', + 'number', + 'undefined', + 'null' + ])); + } + }, + + /** + * The logLevel option validator. + */ + logLevel(property) { + it('should accept integer number values between the 0 and 5', () => { + const obj = { [property]: 0 }; + expect(schema.parse(obj)[property]).toBe(0); + obj[property] = 1; + expect(schema.parse(obj)[property]).toBe(1); + obj[property] = 3; + expect(schema.parse(obj)[property]).toBe(3); + obj[property] = 5; + expect(schema.parse(obj)[property]).toBe(5); + }); + + it('should not accept float number values between the 0 and 5', () => { + const obj = { [property]: 0.1 }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = 1.1; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = 3.1; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = 4.1; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept stringified float number values between the 0 and 5', () => { + const obj = { [property]: '0.1' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '1.1'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '3.1'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '4.1'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept number values that fall outside the 0 and 5 range', () => { + const obj = { [property]: -1.1 }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = 6; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept stringified number number values that fall outside the 0 and 5 range', () => { + const obj = { [property]: '-1.1' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '6'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + if (strictCheck) { + it('should not accept null', () => { + nullThrow(property); + }); + + it('should not accept a stringified undefined', () => { + stringUndefinedThrow(property); + }); + + it('should not accept a stringified null', () => { + stringNullThrow(property); + }); + + it('should not accept an empty string', () => { + emptyStringThrow(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, ['number', 'undefined'])); + } else { + it('should accept stringified number values between the 0 and 5', () => { + const obj = { [property]: '0' }; + expect(schema.parse(obj)[property]).toBe(0); + obj[property] = '1'; + expect(schema.parse(obj)[property]).toBe(1); + obj[property] = '3'; + expect(schema.parse(obj)[property]).toBe(3); + obj[property] = '5'; + expect(schema.parse(obj)[property]).toBe(5); + }); + + it('should accept null', () => { + acceptNull(property); + }); + + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'stringNumber', + 'stringUndefined', + 'stringNull', + 'number', + 'undefined', + 'null' + ])); + } + }, + + /** + * The logFile option validator. + */ + logFile(property, strictCheck) { + it('should accept a string value that ends with the .log extension and is at least one character long without the extension', () => { + const obj = { [property]: 'text.log' }; + expect(schema.parse(obj)[property]).toBe('text.log'); + obj[property] = 't.log'; + expect(schema.parse(obj)[property]).toBe('t.log'); + }); + + it('should not accept a string value that does not end with the .log extension or is not at least one character long without the extension', () => { + const obj = { [property]: 'text' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '.log'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + if (strictCheck) { + it("should not accept 'false', 'undefined', 'null', '' values", () => { + const obj = { [property]: 'false' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = 'undefined'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = 'null'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = ''; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept null', () => { + nullThrow(property); + }); + + it('should not accept a stringified undefined', () => { + stringUndefinedThrow(property); + }); + + it('should not accept a stringified null', () => { + stringNullThrow(property); + }); + + it('should not accept an empty string', () => { + emptyStringThrow(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'string', + 'stringBoolean', + 'stringNumber', + 'stringBigInt', + 'stringSymbol', + 'stringObject', + 'stringArray', + 'stringFunction', + 'stringOther', + 'undefined' + ])); + } else { + it("should accept 'false', 'undefined', 'null', '' values and trasform to null", () => { + const obj = { [property]: 'false' }; + expect(schema.parse(obj)[property]).toBe(null); + obj[property] = 'undefined'; + expect(schema.parse(obj)[property]).toBe(null); + obj[property] = 'null'; + expect(schema.parse(obj)[property]).toBe(null); + obj[property] = ''; + expect(schema.parse(obj)[property]).toBe(null); + }); + + it('should accept null', () => { + acceptNull(property); + }); + + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'string', + 'stringBoolean', + 'stringNumber', + 'stringBigInt', + 'stringUndefined', + 'stringNull', + 'stringSymbol', + 'stringObject', + 'stringArray', + 'stringFunction', + 'stringOther', + 'undefined', + 'null' + ])); + } + }, + + /** + * The resources option validator. + */ + resources(property) { + it("should accept an object with properties 'js', 'css', and 'files'", () => { + const obj = { [property]: { js: '', css: '', files: [] } }; + expect(schema.parse(obj)[property]).toEqual({ + js: null, + css: null, + files: [] + }); + }); + + it("should accept an object with properties 'js', 'css', and 'files' with null values", () => { + const obj = { [property]: { js: null, css: null, files: null } }; + expect(schema.parse(obj)[property]).toEqual({ + js: null, + css: null, + files: null + }); + }); + + it("should accept a partial object with some properties from the 'js', 'css', and 'files'", () => { + const obj = { [property]: { js: 'console.log(1);' } }; + expect(schema.parse(obj)[property]).toEqual({ js: 'console.log(1);' }); + }); + + it("should accept a stringified object with properties 'js', 'css', and 'files'", () => { + const obj = { [property]: "{ js: '', css: '', files: [] }" }; + expect(schema.parse(obj)[property]).toBe( + "{ js: '', css: '', files: [] }" + ); + }); + + it("should accept a stringified object with properties 'js', 'css', and 'files' with null values", () => { + const obj = { [property]: '{ js: null, css: null, files: null }' }; + expect(schema.parse(obj)[property]).toBe( + '{ js: null, css: null, files: null }' + ); + }); + + it("should accept a stringified partial object with some properties from the 'js', 'css', and 'files'", () => { + const obj = { [property]: "{ js: 'console.log(1);' }" }; + expect(schema.parse(obj)[property]).toBe("{ js: 'console.log(1);' }"); + }); + + it('should accept string values that end with .json', () => { + const obj = { [property]: 'resources.json' }; + expect(schema.parse(obj)[property]).toBe('resources.json'); + }); + + it('should not accept string values that do not end with .json', () => { + const obj = { [property]: 'resources.js' }; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept string values that are not at least one character long without the extensions', () => { + const obj = { [property]: '.json' }; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + it('should accept null', () => { + acceptNull(property); + }); + + if (strictCheck) { + it('should not accept a stringified undefined', () => { + stringUndefinedThrow(property); + }); + + it('should not accept a stringified null', () => { + stringNullThrow(property); + }); + + it('should not accept an empty string', () => { + emptyStringThrow(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'stringObject', + 'object', + 'other', + 'undefined', + 'null' + ])); + } else { + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'stringObject', + 'stringUndefined', + 'stringNull', + 'object', + 'other', + 'undefined', + 'null' + ])); + } + }, + + /** + * The createConfig/loadConfig options validator. + */ + customConfig(property, strictCheck) { + it('should accept a string value that ends with the .json extension and is at least one character long without the extension', () => { + const obj = { [property]: 'text.json' }; + expect(schema.parse(obj)[property]).toBe('text.json'); + obj[property] = 't.json'; + expect(schema.parse(obj)[property]).toBe('t.json'); + }); + + it('should not accept a string value that does not end with the .json extension or is not at least one character long without the extension', () => { + const obj = { [property]: 'text' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = '.json'; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + if (strictCheck) { + it("should not accept 'false', 'undefined', 'null', '' values", () => { + const obj = { [property]: 'false' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = 'undefined'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = 'null'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = ''; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept null', () => { + nullThrow(property); + }); + + it('should not accept a stringified undefined', () => { + stringUndefinedThrow(property); + }); + + it('should not accept a stringified null', () => { + stringNullThrow(property); + }); + + it('should not accept an empty string', () => { + emptyStringThrow(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'string', + 'stringBoolean', + 'stringNumber', + 'stringBigInt', + 'stringSymbol', + 'stringObject', + 'stringArray', + 'stringFunction', + 'stringOther', + 'undefined' + ])); + } else { + it("should accept 'false', 'undefined', 'null', '' values and trasform to null", () => { + const obj = { [property]: 'false' }; + expect(schema.parse(obj)[property]).toBe(null); + obj[property] = 'undefined'; + expect(schema.parse(obj)[property]).toBe(null); + obj[property] = 'null'; + expect(schema.parse(obj)[property]).toBe(null); + obj[property] = ''; + expect(schema.parse(obj)[property]).toBe(null); + }); + + it('should accept null', () => { + acceptNull(property); + }); + + it('should accept a stringified undefined and transform it to null', () => { + stringUndefinedToNull(property); + }); + + it('should accept a stringified null and transform it to null', () => { + stringNullToNull(property); + }); + + it('should accept an empty string and transform it to null', () => { + emptyStringToNull(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'emptyString', + 'string', + 'stringBoolean', + 'stringNumber', + 'stringBigInt', + 'stringUndefined', + 'stringNull', + 'stringSymbol', + 'stringObject', + 'stringArray', + 'stringFunction', + 'stringOther', + 'undefined', + 'null' + ])); + } + }, + + /** + * The config object validator. + */ + configObject(property, value) { + it(`should accept an object with the ${property} properties`, () => { + const obj = { [property]: value }; + expect(schema.parse(obj)[property]).toEqual(value); + }); + + it(`should accept an object with the ${property} properties and filter out other properties`, () => { + const obj = { [property]: { ...value, extraProp: true } }; + expect(schema.parse(obj)[property]).toEqual({ ...value }); + }); + + it(`should accept a partial object with some ${property} properties`, () => { + for (const [key, val] of Object.entries(value)) { + expect( + schema.parse({ [property]: { [key]: val } })[property] + ).toEqual({ [key]: val }); + } + expect( + schema.parse({ [property]: { extraProp: true } })[property] + ).toEqual({}); + }); + + it('should accept object with no properties and transform it to undefined', () => { + const obj = {}; + expect(schema.parse(obj)[property]).toBe(undefined); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, [ + 'undefined', + 'object', + 'other' + ])); + }, + + /** + * The requestId validator. + */ + requestId(property) { + it('should accept a correct UUID string value', () => { + const obj = { [property]: 'b012bde4-8b91-4d68-8b48-cd099358a17f' }; + expect(schema.parse(obj)[property]).toBe( + 'b012bde4-8b91-4d68-8b48-cd099358a17f' + ); + obj[property] = '0694de13-ac56-44f9-813c-1c91674e6a19'; + expect(schema.parse(obj)[property]).toBe( + '0694de13-ac56-44f9-813c-1c91674e6a19' + ); + }); + + it('should accept undefined', () => { + acceptUndefined(property); + }); + + it('should accept null', () => { + acceptNull(property); + }); + + it("should not accept 'false', 'undefined', 'null', '' values", () => { + const obj = { [property]: 'false' }; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = 'undefined'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = 'null'; + expect(() => schema.parse(obj)).toThrow(); + obj[property] = ''; + expect(() => schema.parse(obj)).toThrow(); + }); + + it('should not accept a stringified undefined', () => { + stringUndefinedThrow(property); + }); + + it('should not accept a stringified null', () => { + stringNullThrow(property); + }); + + it('should not accept an empty string', () => { + emptyStringThrow(property); + }); + + it('should not accept values of other types', () => + validatePropOfSchema(schema, property, ['null', 'undefined'])); + } + }; + + // The options config validation tests + return { + requestId: (property) => { + describe(property, () => validationTests.requestId(property)); + }, + puppeteer: (property, value) => { + describe(property, () => validationTests.configObject(property, value)); + }, + puppeteerArgs: (property, value, filteredValue) => { + describe(property, () => + validationTests.stringArray(property, value, filteredValue, ';') + ); + }, + highcharts: (property, value) => { + describe(property, () => validationTests.configObject(property, value)); + }, + highchartsVersion: (property) => { + describe(property, () => validationTests.version(property)); + }, + highchartsCdnUrl: (property, correctValue, incorrectValue) => { + describe(property, () => + validationTests.acceptValues(property, correctValue, incorrectValue) + ); + }, + highchartsForceFetch: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + highchartsCachePath: (property) => { + describe(property, () => validationTests.string(property, strictCheck)); + }, + highchartsAdminToken: (property) => { + describe(property, () => validationTests.string(property, false)); + }, + highchartsCoreScripts: (property, value, filteredValue) => { + describe(property, () => + validationTests.stringArray(property, value, filteredValue) + ); + }, + highchartsModuleScripts: (property, value, filteredValue) => { + describe(property, () => + validationTests.stringArray(property, value, filteredValue) + ); + }, + highchartsIndicatorScripts: (property, value, filteredValue) => { + describe(property, () => + validationTests.stringArray(property, value, filteredValue) + ); + }, + highchartsCustomScripts: (property, value, filteredValue) => { + describe(property, () => + validationTests.stringArray(property, value, filteredValue) + ); + }, + export: (property, value) => { + describe(property, () => validationTests.configObject(property, value)); + }, + exportInfile: (property) => { + describe(property, () => validationTests.infile(property)); + }, + exportInstr: (property) => { + describe(property, () => validationTests.chartConfig(property, false)); + }, + exportOptions: (property) => { + describe(property, () => validationTests.chartConfig(property, false)); + }, + exportSvg: (property) => { + describe(property, () => validationTests.svg(property)); + }, + exportOutfile: (property) => { + describe(property, () => validationTests.outfile(property)); + }, + exportType: (property, correctValue, incorrectValue) => { + describe(property, () => + validationTests.acceptValues(property, correctValue, incorrectValue) + ); + }, + exportConstr: (property, correctValue, incorrectValue) => { + describe(property, () => + validationTests.acceptValues(property, correctValue, incorrectValue) + ); + }, + exportB64: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + exportNoDownload: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + exportDefaultHeight: (property) => { + describe(property, () => validationTests.positiveNum(property)); + }, + exportDefaultWidth: (property) => { + describe(property, () => validationTests.positiveNum(property)); + }, + exportDefaultScale: (property) => { + describe(property, () => validationTests.scale(property)); + }, + exportHeight: (property) => { + describe(property, () => validationTests.nullablePositiveNum(property)); + }, + exportWidth: (property) => { + describe(property, () => validationTests.nullablePositiveNum(property)); + }, + exportScale: (property) => { + describe(property, () => validationTests.nullableScale(property)); + }, + exportGlobalOptions: (property) => { + describe(property, () => + validationTests.additionalOptions(property, false) + ); + }, + exportThemeOptions: (property) => { + describe(property, () => + validationTests.additionalOptions(property, false) + ); + }, + exportBatch: (property) => { + describe(property, () => validationTests.string(property, false)); + }, + exportRasterizationTimeout: (property) => { + describe(property, () => validationTests.nonNegativeNum(property)); + }, + customLogic: (property, value) => { + describe(property, () => validationTests.configObject(property, value)); + }, + customLogicAllowCodeExecution: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + customLogicAllowFileResources: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + customLogicCustomCode: (property) => { + describe(property, () => validationTests.string(property, false)); + }, + customLogicCallback: (property) => { + describe(property, () => validationTests.string(property, false)); + }, + customLogicResources: (property) => { + describe(property, () => validationTests.resources(property)); + }, + customLogicLoadConfig: (property) => { + describe(property, () => validationTests.customConfig(property, false)); + }, + customLogicCreateConfig: (property) => { + describe(property, () => validationTests.customConfig(property, false)); + }, + server: (property, value) => { + describe(property, () => validationTests.configObject(property, value)); + }, + serverEnable: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + serverHost: (property) => { + describe(property, () => validationTests.string(property, strictCheck)); + }, + serverPort: (property) => { + describe(property, () => validationTests.nonNegativeNum(property)); + }, + serverBenchmarking: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + serverProxy: (property, value) => { + describe(property, () => validationTests.configObject(property, value)); + }, + serverProxyHost: (property) => { + describe(property, () => validationTests.string(property, false)); + }, + serverProxyPort: (property) => { + describe(property, () => + validationTests.nullableNonNegativeNum(property) + ); + }, + serverProxyTimeout: (property) => { + describe(property, () => validationTests.nonNegativeNum(property)); + }, + serverRateLimiting: (property, value) => { + describe(property, () => validationTests.configObject(property, value)); + }, + serverRateLimitingEnable: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + serverRateLimitingMaxRequests: (property) => { + describe(property, () => validationTests.nonNegativeNum(property)); + }, + serverRateLimitingWindow: (property) => { + describe(property, () => validationTests.nonNegativeNum(property)); + }, + serverRateLimitingDelay: (property) => { + describe(property, () => validationTests.nonNegativeNum(property)); + }, + serverRateLimitingTrustProxy: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + serverRateLimitingSkipKey: (property) => { + describe(property, () => validationTests.string(property, false)); + }, + serverRateLimitingSkipToken: (property) => { + describe(property, () => validationTests.string(property, false)); + }, + serverSsl: (property, value) => { + describe(property, () => validationTests.configObject(property, value)); + }, + serverSslEnable: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + serverSslForce: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + serverSslPort: (property) => { + describe(property, () => validationTests.nonNegativeNum(property)); + }, + serverSslCertPath: (property) => { + describe(property, () => validationTests.string(property, false)); + }, + pool: (property, value) => { + describe(property, () => validationTests.configObject(property, value)); + }, + poolMinWorkers: (property) => { + describe(property, () => validationTests.positiveNum(property)); + }, + poolMaxWorkers: (property) => { + describe(property, () => validationTests.positiveNum(property)); + }, + poolWorkLimit: (property) => { + describe(property, () => validationTests.positiveNum(property)); + }, + poolAcquireTimeout: (property) => { + describe(property, () => validationTests.nonNegativeNum(property)); + }, + poolCreateTimeout: (property) => { + describe(property, () => validationTests.nonNegativeNum(property)); + }, + poolDestroyTimeout: (property) => { + describe(property, () => validationTests.nonNegativeNum(property)); + }, + poolIdleTimeout: (property) => { + describe(property, () => validationTests.nonNegativeNum(property)); + }, + poolCreateRetryInterval: (property) => { + describe(property, () => validationTests.nonNegativeNum(property)); + }, + poolReaperInterval: (property) => { + describe(property, () => validationTests.nonNegativeNum(property)); + }, + poolBenchmarking: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + logging: (property, value) => { + describe(property, () => validationTests.configObject(property, value)); + }, + loggingLevel: (property) => { + describe(property, () => validationTests.logLevel(property, strictCheck)); + }, + loggingFile: (property) => { + describe(property, () => validationTests.logFile(property, strictCheck)); + }, + loggingDest: (property) => { + describe(property, () => validationTests.string(property, strictCheck)); + }, + loggingToConsole: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + loggingToFile: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + ui: (property, value) => { + describe(property, () => validationTests.configObject(property, value)); + }, + uiEnable: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + uiRoute: (property, correctValue, incorrectValue) => { + describe(property, () => + validationTests.acceptValues(property, correctValue, incorrectValue) + ); + }, + other: (property, value) => { + describe(property, () => validationTests.configObject(property, value)); + }, + otherNodeEnv: (property, correctValue, incorrectValue) => { + describe(property, () => + validationTests.acceptValues(property, correctValue, incorrectValue) + ); + }, + otherListenToProcessExits: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + otherNoLogo: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + otherHardResetPage: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + otherBrowserShellMode: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + otherValidation: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + debug: (property, value) => { + describe(property, () => validationTests.configObject(property, value)); + }, + debugEnable: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + debugHeadless: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + debugDevtools: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + debugListenToConsole: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + debugDumpio: (property) => { + describe(property, () => validationTests.boolean(property)); + }, + debugSlowMo: (property) => { + describe(property, () => validationTests.nonNegativeNum(property)); + }, + debugDebuggingPort: (property) => { + describe(property, () => validationTests.nonNegativeNum(property)); + } + }; +} diff --git a/tests/utils/testUtils.js b/tests/utils/testUtils.js new file mode 100644 index 00000000..773969a3 --- /dev/null +++ b/tests/utils/testUtils.js @@ -0,0 +1,129 @@ +/******************************************************************************* + +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 Provides utilities for Jest tests to validate schema properties + * and test value parsing. This module includes a comprehensive set + * of categorized test values, functions for filtering these values based + * on type categories, and a utility for validating specific schema properties + * against allowed and disallowed types. + */ + +import { expect } from '@jest/globals'; + +/** + * A collection of possible values for various data types used in Jest tests, + * categorized by specific types and their stringified versions. + */ +export const possibleValues = { + // String + emptyString: [''], + string: ['string', '1.0.1.0.1'], + + // Boolean + boolean: [true, false], + stringBoolean: ['true', 'false'], + + // Number + number: [0, 0.1, 1, -1, -0.1, NaN], + stringNumber: ['0', '0.1', '1', '-0.1', 'NaN'], + + // BigInt + bigInt: [BigInt(1)], + stringBigInt: ['BigInt(1)'], + + // Undefined + undefined: [undefined], + stringUndefined: ['undefined'], + + // Null + null: [null], + stringNull: ['null'], + + // Symbol + symbol: [Symbol('a')], + stringSymbol: ["Symbol('a')"], + + // Object + object: [{}, { a: 1 }, { a: '1', b: { c: 3 } }], + stringObject: ['{}', '{ a: 1 }', '{ a: "1", b: { c: 3 } }'], + + // Array objects + array: [[], [1], ['a'], [{ a: 1 }]], + stringArray: ['[]', '[1]', '["a"]', '[{ a: 1 }]'], + + // Function objects + function: [function () {}, () => {}], + stringFunction: ['function () {}', '() => {}'], + + // Other objects + other: [new Date(), new Error(''), new RegExp('abc')], + stringOther: ['new Date()', 'new Error("")', 'new RegExp("abc")'] +}; + +/** + * Filters values from an object based on specified categories. + * + * The function iterates over the entries of the `values` object and collects + * items from the arrays that correspond to keys not present in the `categories` + * array. + * + * @function excludeFromValues + * + * @param {Object} values - An object where keys are category names and values + * are arrays of items. + * @param {Array.} [categories=[]] - An array of category names to be + * excluded from the values. + * + * @returns {Array.<*>} An array of items from the `values` object that are not + * in the specified `categories`. + */ +export function excludeFromValues(values, categories = []) { + const filteredArray = []; + Object.entries(values).forEach(([key, value]) => { + if (!categories.includes(key)) { + filteredArray.push(...value); + } + }); + return filteredArray; +} + +/** + * Validates a specific property of a provided schema by testing it against + * various values and ensuring that values not matching the types in the + * `filterTypes` list throw errors. + * + * The function filters the `possibleValues` object to exclude values of the + * types specified in the `filterTypes` array. It then iterates over the + * remaining values and ensures that parsing those values for the specified + * property in the schema results in an error. + * + * @function validatePropOfSchema + * + * @param {ZodSchema} schema - The Zod schema object to be validated. + * @param {string} property - The property of the schema to be validated. + * @param {Array.} [filterTypes=[]] - An array of type categories to be + * excluded from validation. Values of these types will be skipped in the + * validation process. + */ +export function validatePropOfSchema(schema, property, filterTypes = []) { + // Filter the possibleValues object to exclude values of types from + // the filterTypes array + const otherValues = excludeFromValues(possibleValues, filterTypes); + + // Ensure all other values fail validation + otherValues.forEach((value) => { + expect(() => schema.parse({ [property]: value })).toThrow(); + }); +}