diff --git a/test/config.test.js b/test/config.test.js index 15de8bb67d..9eef1d4c96 100644 --- a/test/config.test.js +++ b/test/config.test.js @@ -6,18 +6,13 @@ 'use strict'; -var cp = require('child_process'); -var fs = require('fs'); var IncomingMessage = require('http').IncomingMessage; -var os = require('os'); -var path = require('path'); var util = require('util'); var test = require('tape'); const Agent = require('../lib/agent'); const { MockAPMServer } = require('./_mock_apm_server'); -const { MockLogger } = require('./_mock_logger'); const { NoopApmClient } = require('../lib/apm-client/noop-apm-client'); const { ENV_TABLE } = require('../lib/config/schema'); const config = require('../lib/config/config'); @@ -103,290 +98,6 @@ test('should overwrite option property active by ELASTIC_APM_ACTIVE', function ( t.end(); }); -test('invalid serviceName => inactive', function (t) { - const logger = new MockLogger(); - const agent = new Agent(); - - agent.start( - Object.assign({}, agentOptsNoopTransport, { - serviceName: 'foo&bar', - logger, - }), - ); - - const error = logger.calls.find((log) => log.type === 'error'); - t.ok( - error && error.message.indexOf('serviceName') !== -1, - 'there was a log.error mentioning "serviceName"', - ); - t.strictEqual(agent._conf.active, false, 'active is false'); - agent.destroy(); - t.end(); -}); - -test('valid serviceName => active', function (t) { - var agent = new Agent(); - agent.start( - Object.assign({}, agentOptsNoopTransport, { - serviceName: 'fooBAR0123456789_- ', - }), - ); - t.strictEqual(agent._conf.active, true); - agent.destroy(); - t.end(); -}); - -test('serviceName/serviceVersion zero-conf: valid', function (t) { - cp.execFile( - process.execPath, - ['index.js'], - { - timeout: 3000, - cwd: path.join(__dirname, 'fixtures', 'pkg-zero-conf-valid'), - }, - function (err, stdout, stderr) { - t.error(err, 'no error running index.js: ' + err); - t.equal(stderr, '', 'no stderr'); - const lines = stdout.trim().split('\n'); - const conf = JSON.parse(lines[lines.length - 1]); - t.equal( - conf.serviceName, - 'validName', - 'serviceName was inferred from package.json', - ); - t.equal( - conf.serviceVersion, - '1.2.3', - 'serviceVersion was inferred from package.json', - ); - t.end(); - }, - ); -}); - -test('serviceName/serviceVersion zero-conf: cwd is outside package tree', function (t) { - const indexJs = path.join( - __dirname, - 'fixtures', - 'pkg-zero-conf-valid', - 'index.js', - ); - cp.execFile( - process.execPath, - [indexJs], - { - timeout: 3000, - // Set CWD to outside of the package tree to test whether the agent - // package.json searching uses `require.main`. - cwd: '/', - }, - function (err, stdout, stderr) { - t.error(err, 'no error running index.js: ' + err); - t.equal(stderr, '', 'no stderr'); - const lines = stdout.trim().split('\n'); - const conf = JSON.parse(lines[lines.length - 1]); - t.equal( - conf.serviceName, - 'validName', - 'serviceName was inferred from package.json', - ); - t.equal( - conf.serviceVersion, - '1.2.3', - 'serviceVersion was inferred from package.json', - ); - t.end(); - }, - ); -}); - -test('serviceName/serviceVersion zero-conf: no "name" in package.json', function (t) { - cp.execFile( - process.execPath, - ['index.js'], - { - timeout: 3000, - cwd: path.join(__dirname, 'fixtures', 'pkg-zero-conf-noname'), - }, - function (err, stdout, stderr) { - t.error(err, 'no error running index.js: ' + err); - t.equal(stderr, '', 'no stderr'); - const lines = stdout.trim().split('\n'); - const conf = JSON.parse(lines[lines.length - 1]); - t.equal( - conf.serviceName, - 'unknown-nodejs-service', - 'serviceName is the `unknown-{service.agent.name}-service` zero-conf fallback', - ); - t.equal( - conf.serviceVersion, - '1.2.3', - 'serviceVersion was inferred from package.json', - ); - t.end(); - }, - ); -}); - -// A package.json#name that uses a scoped npm name, e.g. @ns/name, should get -// a normalized serviceName='ns-name'. -test('serviceName/serviceVersion zero-conf: namespaced package name', function (t) { - cp.execFile( - process.execPath, - ['index.js'], - { - timeout: 3000, - cwd: path.join(__dirname, 'fixtures', 'pkg-zero-conf-nsname'), - }, - function (err, stdout, stderr) { - t.error(err, 'no error running index.js: ' + err); - t.equal(stderr, '', 'no stderr'); - const lines = stdout.trim().split('\n'); - const conf = JSON.parse(lines[lines.length - 1]); - t.equal( - conf.serviceName, - 'ns-name', - 'serviceName was inferred and normalized from package.json', - ); - t.equal( - conf.serviceVersion, - '1.2.3', - 'serviceVersion was inferred from package.json', - ); - t.end(); - }, - ); -}); - -test('serviceName/serviceVersion zero-conf: a package name that requires sanitization', function (t) { - cp.execFile( - process.execPath, - ['index.js'], - { - timeout: 3000, - cwd: path.join(__dirname, 'fixtures', 'pkg-zero-conf-sanitize'), - }, - function (err, stdout, stderr) { - t.error(err, 'no error running index.js: ' + err); - t.equal(stderr, '', 'no stderr'); - const lines = stdout.trim().split('\n'); - const conf = JSON.parse(lines[lines.length - 1]); - // serviceName sanitization changes any disallowed char to an underscore. - // The pkg-zero-conf-sanitize/package.json has a name starting with the - // 7 characters that an npm package name can have, but a serviceName - // cannot. - // "name": "~*.!'()validNpmName" - t.equal( - conf.serviceName, - '_______validNpmName', - 'serviceName was inferred and sanitized from package.json', - ); - t.equal( - conf.serviceVersion, - '1.2.3', - 'serviceVersion was inferred from package.json', - ); - t.end(); - }, - ); -}); - -test('serviceName/serviceVersion zero-conf: weird "name" in package.json', function (t) { - cp.execFile( - process.execPath, - ['index.js'], - { - timeout: 3000, - cwd: path.join(__dirname, 'fixtures', 'pkg-zero-conf-weird'), - }, - function (err, stdout, stderr) { - t.error(err, 'no error running index.js: ' + err); - t.equal(stderr, '', 'no stderr'); - const lines = stdout.trim().split('\n'); - const logs = lines.map((l) => JSON.parse(l)); - const logWarn = logs.find((log) => log['log.level'] === 'warn'); - t.ok( - logWarn['log.level'] === 'warn' && - logWarn.message.indexOf('serviceName') !== -1, - 'there is a log.warn about "serviceName"', - ); - const conf = JSON.parse( - // Filter out log lines from the APM agent itself. We just want the - // `console.log(...)` from the index.js script. - lines.filter((ln) => ln.indexOf('"log.level":') === -1)[0], - ); - t.equal( - conf.serviceName, - 'unknown-nodejs-service', - 'serviceName is the `unknown-{service.agent.name}-service` zero-conf fallback', - ); - t.equal( - conf.serviceVersion, - '1.2.3', - 'serviceVersion was inferred from package.json', - ); - t.end(); - }, - ); -}); - -test('serviceName/serviceVersion zero-conf: no package.json to find', function (t) { - // To test the APM agent's fallback serviceName, we need to execute - // a script in a dir that has no package.json in its dir, or any dir up - // from it (we assume/hope that `os.tmpdir()` works for that). - const dir = os.tmpdir(); - const script = path.resolve(dir, 'elastic-apm-node-zero-conf-test-script.js'); - // Avoid Windows '\' path separators that are interpreted as escapes when - // interpolated into the script content below. - const agentDir = path - .resolve(__dirname, '..') - .replace(new RegExp('\\' + path.win32.sep, 'g'), path.posix.sep); - function setupPkgEnv() { - fs.writeFileSync( - script, - ` - const apm = require('${agentDir}').start({ - disableSend: true - }) - console.log(JSON.stringify(apm._conf)) - `, - ); - t.comment(`created ${script}`); - } - function teardownPkgEnv() { - fs.unlinkSync(script); - t.comment(`removed ${script}`); - } - - setupPkgEnv(); - cp.execFile( - process.execPath, - [script], - { - timeout: 3000, - cwd: dir, - }, - function (err, stdout, stderr) { - t.error(err, 'no error running script: ' + err); - t.equal(stderr, '', 'no stderr'); - const lines = stdout.trim().split('\n'); - const conf = JSON.parse( - // Filter out log lines from the APM agent itself. We just want the - // `console.log(...)` from the index.js script. - lines.filter((ln) => ln.indexOf('"log.level":') === -1)[0], - ); - t.equal( - conf.serviceName, - 'unknown-nodejs-service', - 'serviceName is the `unknown-{service.agent.name}-service` zero-conf fallback', - ); - t.equal(conf.serviceVersion, undefined, 'serviceVersion is undefined'); - teardownPkgEnv(); - t.end(); - }, - ); -}); - var captureBodyTests = [ { value: 'off', errors: '[REDACTED]', transactions: '[REDACTED]' }, { value: 'transactions', errors: '[REDACTED]', transactions: 'test' }, diff --git a/test/config/config.test.js b/test/config/config.test.js index baecf73a50..42656e1ea1 100644 --- a/test/config/config.test.js +++ b/test/config/config.test.js @@ -6,13 +6,17 @@ 'use strict'; +const path = require('path'); +var fs = require('fs'); +var os = require('os'); + const test = require('tape'); const { normalize } = require('../../lib/config/config'); const { CONFIG_SCHEMA, getDefaultOptions } = require('../../lib/config/schema'); const { runTestFixtures } = require('../_utils'); -const { reviver, replacer } = require('./_json-utils.js'); +const { reviver, replacer } = require('./json-utils.js'); const defaultOptions = getDefaultOptions(); @@ -448,7 +452,7 @@ const testFixtures = [ t.deepEqual( resolvedConfig.globalLabels, pairs, - 'globalLabels is parsed correctly from environment (array)', + 'globalLabels is parsed correctly from start options (array)', ); }, }, @@ -577,7 +581,7 @@ const testFixtures = [ t.deepEqual( resolvedConfig.ignoreUrlStr, ['str1'], - 'string items of ignoreUrl are added to the right config (ignoreUrlStr)', + 'string items of ignoreUrls are added to the right config (ignoreUrlStr)', ); t.deepEqual( resolvedConfig.ignoreUrlRegExp, @@ -596,6 +600,267 @@ const testFixtures = [ ); }, }, + { + name: 'use agent - should not be active if the service name is invalid', + script: 'fixtures/use-agent.js', + cwd: __dirname, + noConvenienceConfig: true, + env: { + ELASTIC_APM_SERVICE_NAME: 'foo&bar', + }, + checkScriptResult: (t, err, stdout) => { + t.error(err, `use-agent.js script succeeded: err=${err}`); + const useAgentLogs = getUseAgentLogs(stdout); + const agentErrors = getApmLogs(stdout, 'error'); + const resolvedConfig = JSON.parse(useAgentLogs[2], reviver); + + t.equal( + agentErrors[0].message, + 'serviceName "foo&bar" is invalid: contains invalid characters (allowed: a-z, A-Z, 0-9, _, -, )', + 'agent shows an error about the bogs service name', + ); + t.equal( + agentErrors[1].message, + 'Elastic APM is incorrectly configured: Missing serviceName (APM will be disabled)', + 'agent shows an message telling it will be disabled', + ); + t.equal( + resolvedConfig.active, + false, + 'active property of configuration is false', + ); + }, + }, + { + name: 'use agent - should be active if the service name valid', + script: 'fixtures/use-agent.js', + cwd: __dirname, + noConvenienceConfig: true, + env: { + ELASTIC_APM_SERVICE_NAME: 'fooBAR0123456789_- ', + }, + checkScriptResult: (t, err, stdout) => { + t.error(err, `use-agent.js script succeeded: err=${err}`); + const useAgentLogs = getUseAgentLogs(stdout); + const agentErrors = getApmLogs(stdout, 'error'); + const resolvedConfig = JSON.parse(useAgentLogs[2], reviver); + + t.equal(agentErrors.length, 0, 'agent shows no errors'); + t.equal( + resolvedConfig.active, + true, + 'active property of configuration is true', + ); + }, + }, + { + name: 'pkg-zero-conf-valid - serviceName/serviceVersion inferred from package.json', + script: 'fixtures/pkg-zero-conf-valid/index.js', + cwd: __dirname, + noConvenienceConfig: true, + checkScriptResult: (t, err, stdout) => { + t.error(err, `pkg-zero-conf-valid/index.js script succeeded: err=${err}`); + const lines = stdout.trim().split('\n'); + const conf = JSON.parse(lines[lines.length - 1]); + t.equal( + conf.serviceName, + 'validName', + 'serviceName was inferred from package.json', + ); + t.equal( + conf.serviceVersion, + '1.2.3', + 'serviceVersion was inferred from package.json', + ); + }, + }, + { + name: 'pkg-zero-conf-valid - serviceName/serviceVersion inferred from package.json even if cwd is out the package tree', + script: path.resolve(__dirname, 'fixtures/pkg-zero-conf-valid/index.js'), + cwd: '/', + noConvenienceConfig: true, + checkScriptResult: (t, err, stdout) => { + t.error(err, `pkg-zero-conf-valid/index.js script succeeded: err=${err}`); + const lines = stdout.trim().split('\n'); + const conf = JSON.parse(lines[lines.length - 1]); + t.equal( + conf.serviceName, + 'validName', + 'serviceName was inferred from package.json', + ); + t.equal( + conf.serviceVersion, + '1.2.3', + 'serviceVersion was inferred from package.json', + ); + }, + }, + { + name: 'pkg-zero-conf-noname - serviceName/serviceVersion inferred from package.json even if there is no "name"', + script: 'fixtures/pkg-zero-conf-noname/index.js', + cwd: __dirname, + noConvenienceConfig: true, + checkScriptResult: (t, err, stdout) => { + t.error( + err, + `pkg-zero-conf-noname/index.js script succeeded: err=${err}`, + ); + const lines = stdout.trim().split('\n'); + const conf = JSON.parse(lines[lines.length - 1]); + t.equal( + conf.serviceName, + 'unknown-nodejs-service', + 'serviceName was inferred from package.json wihtout "name"', + ); + t.equal( + conf.serviceVersion, + '1.2.3', + 'serviceVersion was inferred from package.json', + ); + }, + }, + // A package.json#name that uses a scoped npm name, e.g. @ns/name, should get + // a normalized serviceName='ns-name'. + { + name: 'pkg-zero-conf-nsname - serviceName/serviceVersion inferred from namespaced package', + script: 'fixtures/pkg-zero-conf-nsname/index.js', + cwd: __dirname, + noConvenienceConfig: true, + checkScriptResult: (t, err, stdout) => { + t.error( + err, + `pkg-zero-conf-nsname/index.js script succeeded: err=${err}`, + ); + const lines = stdout.trim().split('\n'); + const conf = JSON.parse(lines[lines.length - 1]); + t.equal( + conf.serviceName, + 'ns-name', + 'serviceName was inferred and normalized from package.json', + ); + t.equal( + conf.serviceVersion, + '1.2.3', + 'serviceVersion was inferred from package.json', + ); + }, + }, + { + name: 'pkg-zero-conf-sanitize - serviceName/serviceVersion inferred package.json and sanitized', + script: 'fixtures/pkg-zero-conf-sanitize/index.js', + cwd: __dirname, + noConvenienceConfig: true, + checkScriptResult: (t, err, stdout) => { + t.error( + err, + `pkg-zero-conf-sanitize/index.js script succeeded: err=${err}`, + ); + const lines = stdout.trim().split('\n'); + const conf = JSON.parse(lines[lines.length - 1]); + // serviceName sanitization changes any disallowed char to an underscore. + // The pkg-zero-conf-sanitize/package.json has a name starting with the + // 7 characters that an npm package name can have, but a serviceName + // cannot. + // "name": "~*.!'()validNpmName" + t.equal( + conf.serviceName, + '_______validNpmName', + 'serviceName was inferred and sanitized from package.json', + ); + t.equal( + conf.serviceVersion, + '1.2.3', + 'serviceVersion was inferred from package.json', + ); + }, + }, + { + name: 'pkg-zero-conf-weird - serviceName/serviceVersion inferred package.json and weirdd', + script: 'fixtures/pkg-zero-conf-weird/index.js', + cwd: __dirname, + noConvenienceConfig: true, + checkScriptResult: (t, err, stdout) => { + t.error(err, `pkg-zero-conf-weird/index.js script succeeded: err=${err}`); + const lines = stdout.trim().split('\n'); + const logs = lines.map((l) => JSON.parse(l)); + const logWarn = logs.find((log) => log['log.level'] === 'warn'); + t.ok( + logWarn['log.level'] === 'warn' && + logWarn.message.indexOf('serviceName') !== -1, + 'there is a log.warn about "serviceName"', + ); + const conf = JSON.parse( + // Filter out log lines from the APM agent itself. We just want the + // `console.log(...)` from the index.js script. + lines.filter((ln) => ln.indexOf('"log.level":') === -1)[0], + ); + t.equal( + conf.serviceName, + 'unknown-nodejs-service', + 'serviceName is the `unknown-{service.agent.name}-service` zero-conf fallback', + ); + t.equal( + conf.serviceVersion, + '1.2.3', + 'serviceVersion was inferred from package.json', + ); + }, + }, + { + name: 'pkg-zero-conf-none - serviceName/serviceVersion resolved to unknown if no package.json present', + script: (function () { + const path = require('path'); + const fs = require('fs'); + const os = require('os'); + + // To test the APM agent's fallback serviceName, we need to execute + // a script in a dir that has no package.json in its dir, or any dir up + // from it (we assume/hope that `os.tmpdir()` works for that). + const script = path.resolve( + os.tmpdir(), + 'elastic-apm-node-zero-conf-test-script.js', + ); + // Avoid Windows '\' path separators that are interpreted as escapes when + // interpolated into the script content below. + const agentDir = path + .resolve(__dirname, '..', '..') + .replace(new RegExp('\\' + path.win32.sep, 'g'), path.posix.sep); + + fs.writeFileSync( + script, + `const apm = require('${agentDir}').start({ + disableSend: true + }) + console.log(JSON.stringify(apm._conf))`, + ); + return script; + })(), + cwd: os.tmpdir(), + noConvenienceConfig: true, + checkScriptResult: (t, err, stdout) => { + t.error( + err, + `elastic-apm-node-zero-conf-test-script.js script succeeded: err=${err}`, + ); + const lines = stdout.trim().split('\n'); + const conf = JSON.parse( + // Filter out log lines from the APM agent itself. We just want the + // `console.log(...)` from the index.js script. + lines.filter((ln) => ln.indexOf('"log.level":') === -1)[0], + ); + t.equal( + conf.serviceName, + 'unknown-nodejs-service', + 'serviceName is the `unknown-{service.agent.name}-service` zero-conf fallback', + ); + t.equal(conf.serviceVersion, undefined, 'serviceVersion is undefined'); + + // remove the script + fs.unlinkSync( + path.resolve(os.tmpdir(), 'elastic-apm-node-zero-conf-test-script.js'), + ); + }, + }, ]; test('agent config fixtures', (suite) => { diff --git a/test/fixtures/pkg-zero-conf-noname/index.js b/test/config/fixtures/pkg-zero-conf-noname/index.js similarity index 87% rename from test/fixtures/pkg-zero-conf-noname/index.js rename to test/config/fixtures/pkg-zero-conf-noname/index.js index 8910751d8a..c93709a8d9 100644 --- a/test/fixtures/pkg-zero-conf-noname/index.js +++ b/test/config/fixtures/pkg-zero-conf-noname/index.js @@ -4,7 +4,7 @@ * compliance with the BSD 2-Clause License. */ -const apm = require('../../..').start({ +const apm = require('../../../..').start({ disableSend: true, logLevel: 'off', }); diff --git a/test/fixtures/pkg-zero-conf-noname/package.json b/test/config/fixtures/pkg-zero-conf-noname/package.json similarity index 100% rename from test/fixtures/pkg-zero-conf-noname/package.json rename to test/config/fixtures/pkg-zero-conf-noname/package.json diff --git a/test/fixtures/pkg-zero-conf-nsname/index.js b/test/config/fixtures/pkg-zero-conf-nsname/index.js similarity index 87% rename from test/fixtures/pkg-zero-conf-nsname/index.js rename to test/config/fixtures/pkg-zero-conf-nsname/index.js index 8910751d8a..c93709a8d9 100644 --- a/test/fixtures/pkg-zero-conf-nsname/index.js +++ b/test/config/fixtures/pkg-zero-conf-nsname/index.js @@ -4,7 +4,7 @@ * compliance with the BSD 2-Clause License. */ -const apm = require('../../..').start({ +const apm = require('../../../..').start({ disableSend: true, logLevel: 'off', }); diff --git a/test/fixtures/pkg-zero-conf-nsname/package.json b/test/config/fixtures/pkg-zero-conf-nsname/package.json similarity index 100% rename from test/fixtures/pkg-zero-conf-nsname/package.json rename to test/config/fixtures/pkg-zero-conf-nsname/package.json diff --git a/test/fixtures/pkg-zero-conf-sanitize/index.js b/test/config/fixtures/pkg-zero-conf-sanitize/index.js similarity index 87% rename from test/fixtures/pkg-zero-conf-sanitize/index.js rename to test/config/fixtures/pkg-zero-conf-sanitize/index.js index 8910751d8a..c93709a8d9 100644 --- a/test/fixtures/pkg-zero-conf-sanitize/index.js +++ b/test/config/fixtures/pkg-zero-conf-sanitize/index.js @@ -4,7 +4,7 @@ * compliance with the BSD 2-Clause License. */ -const apm = require('../../..').start({ +const apm = require('../../../..').start({ disableSend: true, logLevel: 'off', }); diff --git a/test/fixtures/pkg-zero-conf-sanitize/package.json b/test/config/fixtures/pkg-zero-conf-sanitize/package.json similarity index 100% rename from test/fixtures/pkg-zero-conf-sanitize/package.json rename to test/config/fixtures/pkg-zero-conf-sanitize/package.json diff --git a/test/fixtures/pkg-zero-conf-valid/index.js b/test/config/fixtures/pkg-zero-conf-valid/index.js similarity index 87% rename from test/fixtures/pkg-zero-conf-valid/index.js rename to test/config/fixtures/pkg-zero-conf-valid/index.js index 8910751d8a..c93709a8d9 100644 --- a/test/fixtures/pkg-zero-conf-valid/index.js +++ b/test/config/fixtures/pkg-zero-conf-valid/index.js @@ -4,7 +4,7 @@ * compliance with the BSD 2-Clause License. */ -const apm = require('../../..').start({ +const apm = require('../../../..').start({ disableSend: true, logLevel: 'off', }); diff --git a/test/fixtures/pkg-zero-conf-valid/package.json b/test/config/fixtures/pkg-zero-conf-valid/package.json similarity index 100% rename from test/fixtures/pkg-zero-conf-valid/package.json rename to test/config/fixtures/pkg-zero-conf-valid/package.json diff --git a/test/fixtures/pkg-zero-conf-weird/index.js b/test/config/fixtures/pkg-zero-conf-weird/index.js similarity index 86% rename from test/fixtures/pkg-zero-conf-weird/index.js rename to test/config/fixtures/pkg-zero-conf-weird/index.js index 9d403c5126..9cef5aa357 100644 --- a/test/fixtures/pkg-zero-conf-weird/index.js +++ b/test/config/fixtures/pkg-zero-conf-weird/index.js @@ -4,7 +4,7 @@ * compliance with the BSD 2-Clause License. */ -const apm = require('../../..').start({ +const apm = require('../../../..').start({ disableSend: true, }); console.log(JSON.stringify(apm._conf)); diff --git a/test/fixtures/pkg-zero-conf-weird/package.json b/test/config/fixtures/pkg-zero-conf-weird/package.json similarity index 100% rename from test/fixtures/pkg-zero-conf-weird/package.json rename to test/config/fixtures/pkg-zero-conf-weird/package.json diff --git a/test/config/fixtures/use-agent.js b/test/config/fixtures/use-agent.js index c7bfbf6d2f..df1177c4b3 100644 --- a/test/config/fixtures/use-agent.js +++ b/test/config/fixtures/use-agent.js @@ -8,7 +8,7 @@ const { NoopApmClient } = require('../../../lib/apm-client/noop-apm-client'); -const { replacer, reviver } = require('../_json-utils'); +const { replacer, reviver } = require('../json-utils'); const APM_START_OPTIONS = { transport: () => new NoopApmClient(), diff --git a/test/config/_json-utils.js b/test/config/json-utils.js similarity index 100% rename from test/config/_json-utils.js rename to test/config/json-utils.js