Skip to content

Commit

Permalink
Merge pull request #484 from highcharts/test/basic-test-env-setup
Browse files Browse the repository at this point in the history
test/basic-test-env-setup
  • Loading branch information
PaulDalek authored Apr 4, 2024
2 parents 56908ad + 8c405da commit c8451d8
Show file tree
Hide file tree
Showing 21 changed files with 3,359 additions and 265 deletions.
8 changes: 8 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ module.exports = {
'plugin:import/recommended',
'plugin:prettier/recommended'
],
overrides: [
{
files: ['*.test.js', '*.spec.js'],
env: {
jest: true
}
}
],
rules: {
'no-unused-vars': 0,
'import/no-cycle': 2,
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-and-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '16.4.0'
node-version: '18.x.x'

- name: Install Dependencies
run: npm ci
Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/eslint-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: ESLint check

on: [ pull_request ]

jobs:
eslint:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18.x.x'

- name: Install Dependencies
run: npm install

- name: Run ESLint
id: eslint
continue-on-error: true
run: |
ESLINT_OUTPUT=$(npx eslint . --ext .js,.jsx,.ts,.tsx)
echo "::set-output name=result::$ESLINT_OUTPUT"
if [ -z "$ESLINT_OUTPUT" ]; then
echo "ESLint found no issues :white_check_mark:"
echo "::set-output name=status::success"
else
echo "$ESLINT_OUTPUT"
echo "::set-output name=status::failure"
exit 1
fi
- name: Success Message
if: steps.eslint.outputs.status == 'success'
run: echo "✅ ESLint check passed successfully!"

- name: Failure Message
if: failure()
run: echo "❌ ESLint check failed. Please fix the issues."
26 changes: 26 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Unit tests

on:
pull_request:
branches: [ master ]

jobs:
testing:
runs-on: ubuntu-latest

steps:
- name: Checkout Repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18.x.x'

- name: Install dependencies
run: npm ci

- name: Run unit tests
run: npm run unit:test
env:
CI: true
5 changes: 5 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run lint
npm run unit:test
4 changes: 2 additions & 2 deletions dist/index.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.esm.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.esm.js.map

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default {
testEnvironment: 'jest-environment-node',
testMatch: ['**/tests/unit/**/*.test.js'],
transform: {
// '^.+\\.test.js?$': 'babel-jest'
}
};
26 changes: 15 additions & 11 deletions lib/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { HttpsProxyAgent } from 'https-proxy-agent';
import { fetch } from './fetch.js';
import { log } from './logger.js';
import { __dirname } from './utils.js';
import { envs } from './envs.js';

import ExportError from './errors/ExportError.js';

Expand All @@ -42,18 +43,19 @@ let appliedConfig = false;
*
* @returns {string} The extracted Highcharts version.
*/
const extractVersion = () =>
(cache.hcVersion = cache.sources
export const extractVersion = (cache) => {
return cache.sources
.substring(0, cache.sources.indexOf('*/'))
.replace('/*', '')
.replace('*/', '')
.replace(/\n/g, '')
.trim());
.trim();
};

/**
* Extracts the Highcharts module name based on the scriptPath.
*/
const extractModuleName = (scriptPath) => {
export const extractModuleName = (scriptPath) => {
return scriptPath.replace(
/(.*)\/|(.*)modules\/|stock\/(.*)indicators\/|maps\/(.*)modules\//gi,
''
Expand All @@ -71,7 +73,7 @@ const extractModuleName = (scriptPath) => {
* @throws {ExportError} Throws an ExportError if an error occurs while writing
* the cache manifest.
*/
const saveConfigToManifest = async (config, fetchedModules) => {
export const saveConfigToManifest = async (config, fetchedModules) => {
const newManifest = {
version: config.version,
modules: fetchedModules || {}
Expand Down Expand Up @@ -108,7 +110,7 @@ const saveConfigToManifest = async (config, fetchedModules) => {
* @throws {ExportError} Throws an ExportError if there is a problem with
* fetching the script.
*/
const fetchAndProcessScript = async (
export const fetchAndProcessScript = async (
script,
proxyAgent,
fetchedModules,
Expand All @@ -125,7 +127,7 @@ const fetchAndProcessScript = async (
const requestOptions = proxyAgent
? {
agent: proxyAgent,
timeout: +process.env['PROXY_SERVER_TIMEOUT'] || 5000
timeout: envs.PROXY_SERVER_TIMEOUT
}
: {};

Expand Down Expand Up @@ -165,7 +167,7 @@ const fetchAndProcessScript = async (
* @param {object} fetchedModules - An object which tracks which Highcharts modules have been fetched.
* @returns {Promise<string>} The fetched scripts content joined.
*/
const fetchScripts = async (
export const fetchScripts = async (
coreScripts,
moduleScripts,
customScripts,
Expand Down Expand Up @@ -207,7 +209,7 @@ const fetchScripts = async (
* @throws {ExportError} Throws an ExportError if there is an issue updating
* the local Highcharts cache.
*/
const updateCache = async (config, sourcePath) => {
export const updateCache = async (config, sourcePath) => {
const { coreScripts, modules, indicators, scripts: customScripts } = config;
const hcVersion =
config.version === 'latest' || !config.version ? '' : `${config.version}/`;
Expand Down Expand Up @@ -253,7 +255,8 @@ const updateCache = async (config, sourcePath) => {
proxyAgent,
fetchedModules
);
extractVersion();

cache.hcVersion = extractVersion(cache);

// Save the fetched modules into caches' source JSON
writeFileSync(sourcePath, cache.sources);
Expand Down Expand Up @@ -374,7 +377,8 @@ export const checkAndUpdateCache = async (config) => {

// Get current modules map
fetchedModules = manifest.modules;
extractVersion();

cache.hcVersion = extractVersion(cache);
}
}

Expand Down
20 changes: 3 additions & 17 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from './schemas/config.js';
import { log, logWithStack } from './logger.js';
import { deepCopy, isObject, printUsage, toBoolean } from './utils.js';
import { envs } from './envs.js';

let generalOptions = {};

Expand Down Expand Up @@ -311,7 +312,6 @@ function updateDefaultConfig(configObj, customObj = {}, propChain = '') {
Object.keys(configObj).forEach((key) => {
const entry = configObj[key];
const customValue = customObj && customObj[key];
let numEnvVal;

if (typeof entry.value === 'undefined') {
updateDefaultConfig(entry, customValue, `${propChain}.${key}`);
Expand All @@ -322,22 +322,8 @@ function updateDefaultConfig(configObj, customObj = {}, propChain = '') {
}

// If a value from an env variable exists, it take precedence
if (entry.envLink) {
// Load the env var
if (entry.type === 'boolean') {
entry.value = toBoolean(
[process.env[entry.envLink], entry.value].find(
(el) => el || el === 'false'
)
);
} else if (entry.type === 'number') {
numEnvVal = +process.env[entry.envLink];
entry.value = numEnvVal >= 0 ? numEnvVal : entry.value;
} else if (entry.type.indexOf(']') >= 0 && process.env[entry.envLink]) {
entry.value = process.env[entry.envLink].split(',');
} else {
entry.value = process.env[entry.envLink] || entry.value;
}
if (entry.envLink in envs) {
entry.value = envs[entry.envLink];
}
}
});
Expand Down
132 changes: 132 additions & 0 deletions lib/envs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* @fileoverview
* 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 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.
*
* For envs not defined in config.js with defaults, we also include default values here (PROXY_...).
*/

import { z } from 'zod';
import dotenv from 'dotenv';
dotenv.config();

// Object with custom validators and transformers, to avoid repetition in the Config object
const v = {
boolean: () =>
z
.enum(['true', 'false'])
.transform((value) => value === 'true')
.optional(),
array: () =>
z
.string()
.transform((val) => val.split(',').map((v) => v.trim()))
.optional()
};

export const Config = z.object({
// highcharts
HIGHCHARTS_VERSION: z
.string()
.refine((value) => /^(latest|\d+(\.\d+){0,2})$/.test(value), {
message:
"HIGHCHARTS_VERSION must be 'latest', a major version, or in the form XX.YY.ZZ"
})
.optional(), // todo: create an array of available Highcharts versions
HIGHCHARTS_CDN_URL: z
.string()
.trim()
.refine((val) => val.startsWith('https://') || val.startsWith('http://'), {
message:
'Invalid value for HIGHCHARTS_CDN_URL. It should start with http:// or https://.'
})
.optional(),
HIGHCHARTS_CORE_SCRIPTS: v.array(),
HIGHCHARTS_MODULES: v.array(),
HIGHCHARTS_INDICATORS: v.array(),
HIGHCHARTS_FORCE_FETCH: v.boolean(),
HIGHCHARTS_CACHE_PATH: z.string().optional(),
HIGHCHARTS_ADMIN_TOKEN: z.string().optional(),

// export
EXPORT_TYPE: z.enum(['jpeg', 'png', 'pdf', 'svg']).optional(),
EXPORT_CONSTR: z
.string()
.refine(
(val) =>
['chart', 'stockChart', 'mapChart', 'ganttChart'].includes(val || ''),
{ message: 'Invalid value for EXPORT_CONSTR. ' }
)
.optional(),
EXPORT_DEFAULT_HEIGHT: z.coerce.number().positive().optional(),
EXPORT_DEFAULT_WIDTH: z.coerce.number().positive().optional(),
EXPORT_DEFAULT_SCALE: z.coerce.number().positive().optional(),
EXPORT_RASTERIZATION_TIMEOUT: z.coerce.number().positive().optional(),

// custom
CUSTOM_LOGIC_ALLOW_CODE_EXECUTION: v.boolean(),
CUSTOM_LOGIC_ALLOW_FILEL_RESOURCES: v.boolean(),

// server-related
SERVER_ENABLE: v.boolean(),
SERVER_HOST: z.string().optional(),
SERVER_PORT: z.coerce.number().optional(),
SERVER_BENCHMARKING: v.boolean(),
SERVER_SSL_ENABLE: v.boolean(),
SERVER_SSL_FORCE: v.boolean(),
SERVER_SSL_PORT: z.coerce.number().optional(),
SERVER_SSL_CERT_PATH: z.string().optional(),
SERVER_RATE_LIMITING_ENABLE: v.boolean(),
SERVER_RATE_LIMITING_MAX_REQUESTS: z.coerce.number().optional(),
SERVER_RATE_LIMITING_WINDOW: z.coerce.number().optional(),
SERVER_RATE_LIMITING_DELAY: z.coerce.number().optional(),
SERVER_RATE_LIMITING_TRUST_PROXY: v.boolean(),
SERVER_RATE_LIMITING_SKIP_KEY: z.string().optional(),
SERVER_RATE_LIMITING_SKIP_TOKEN: z.string().optional(),

// pool
POOL_MIN_WORKERS: z.coerce.number().optional(),
POOL_MAX_WORKERS: z.coerce.number().optional(),
POOL_WORK_LIMIT: z.coerce.number().optional(),
POOL_ACQUIRE_TIMEOUT: z.coerce.number().optional(),
POOL_CREATE_TIMEOUT: z.coerce.number().optional(),
POOL_DESTROY_TIMEOUT: z.coerce.number().optional(),
POOL_IDLE_TIMEOUT: z.coerce.number().optional(),
POOL_CREATE_RETRY_INTERVAL: z.coerce.number().optional(),
POOL_REAPER_INTERVAL: z.coerce.number().optional(),
POOL_BENCHMARKING: v.boolean(),
POOL_LISTEN_TO_PROCESS_EXITS: v.boolean(),

// logger
LOGGING_LEVEL: z.coerce
.number()
.optional()
.refine((val) => (val || 5) >= 0 && (val || 5) <= 5, {
message:
'Invalid value for LOGGING_LEVEL. We only accept 0, 1, 2, 3, 4, 5 as logging levels.'
}),
LOGGING_FILE: z.string().optional(),
LOGGING_DEST: z.string().optional(),

// ui
UI_ENABLE: v.boolean(),
UI_ROUTE: z.string().optional(),

// other
OTHER_NO_LOGO: v.boolean(),
NODE_ENV: z
.enum(['development', 'production', 'test'])
.optional()
.default('production'),

// proxy (! NOT INCLUDED IN CONFIG.JS !)
PROXY_SERVER_TIMEOUT: z.coerce.number().positive().optional().default(5000),
PROXY_SERVER_HOST: z.string().optional().default('localhost'),
PROXY_SERVER_PORT: z.coerce.number().positive().optional().default(8080)
});

export const envs = Config.parse(process.env);
3 changes: 2 additions & 1 deletion lib/server/error.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { logWithStack } from '../logger.js';
import { envs } from '../envs.js';

/**
* Middleware for logging errors with stack trace and handling error response.
Expand All @@ -13,7 +14,7 @@ const logErrorMiddleware = (error, req, res, next) => {
logWithStack(1, error);

// Delete the stack for the environment other than the development
if (process.env.NODE_ENV !== 'development') {
if (envs.NODE_ENV !== 'development') {
delete error.stack;
}

Expand Down
Loading

0 comments on commit c8451d8

Please sign in to comment.