diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54d561a..75b7794 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,8 @@ name: CI on: - push: pull_request: + types: [opened, synchronize, reopened] env: CI: true @@ -11,6 +11,11 @@ jobs: lint: uses: haraka/.github/.github/workflows/lint.yml@master + prettier: + uses: haraka/.github/.github/workflows/prettier.yml@master + with: + branch: ${{ github.head_ref }} + coverage: uses: haraka/.github/.github/workflows/coverage.yml@master secrets: inherit diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8314a66..a2d8232 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -6,8 +6,6 @@ on: pull_request: # The branches below must be a subset of the branches above branches: [master] - schedule: - - cron: '18 7 * * 4' jobs: codeql: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e81c15f..08a6016 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,11 +1,8 @@ name: publish on: - push: - branches: - - master - paths: - - package.json + release: + types: [published] env: CI: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..38e8da0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,17 @@ +name: release + +on: + push: + branches: + - master + - main + paths: + - package.json + +env: + CI: true + +jobs: + release: + uses: haraka/.github/.github/workflows/release.yml@master + secrets: inherit diff --git a/.release b/.release index 0bf2a09..aadda56 160000 --- a/.release +++ b/.release @@ -1 +1 @@ -Subproject commit 0bf2a098d4792848c2103dfce0f911e00a14709e +Subproject commit aadda569387d619f83866e7ed4cf9a67e4076f31 diff --git a/CHANGELOG.md b/CHANGELOG.md index 932351f..a03fe00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ### Unreleased +### [1.5.0] - 2026-03-04 + +- dep: replaced js-yaml with yaml + - yaml supports v1.2 + - js-yaml is barely maintained, replacing it removes most vuln warnings +- test: replaced mocha with node --test +- test: added test/watch +- ci: updated ci, publish & release workflows + ### [1.4.2] - 2025-01-08 - dep(eslint): upgrade 8 -> 9 @@ -156,3 +165,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). [1.4.0]: https://github.com/haraka/haraka-config/releases/tag/v1.4.0 [1.4.1]: https://github.com/haraka/haraka-config/releases/tag/v1.4.1 [1.4.2]: https://github.com/haraka/haraka-config/releases/tag/v1.4.2 +[1.5.0]: https://github.com/haraka/haraka-config/releases/tag/v1.5.0 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 72e9afe..f27a5db 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,8 +1,8 @@ # Contributors -This handcrafted artisinal software is brought to you by: +This handcrafted artisanal software is brought to you by: -|
msimerson (60) |
PSSGCSim (7) |
louis-lau (2) |
baudehlo (1) |
Wesitos (1) |
oreoluwa (1) |
polarismail (1) | +|
msimerson (61) |
PSSGCSim (7) |
louis-lau (2) |
baudehlo (1) |
Wesitos (1) |
oreoluwa (1) |
polarismail (1) | | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | this file is generated by [.release](https://github.com/msimerson/.release). diff --git a/config.js b/config.js index c62184e..372289a 100644 --- a/config.js +++ b/config.js @@ -23,9 +23,7 @@ class Config { let [name, type, cb, options] = this.arrange_args(args) if (!type) type = 'value' - const full_path = path.isAbsolute(name) - ? name - : path.resolve(this.root_path, name) + const full_path = path.isAbsolute(name) ? name : path.resolve(this.root_path, name) let results = reader.read_config(full_path, type, cb, options) @@ -129,11 +127,7 @@ function merge_config(defaults, overrides, type) { return merge_struct(JSON.parse(JSON.stringify(defaults)), overrides) } - if ( - Array.isArray(overrides) && - Array.isArray(defaults) && - overrides.length > 0 - ) { + if (Array.isArray(overrides) && Array.isArray(defaults) && overrides.length > 0) { return overrides } diff --git a/eslint.config.mjs b/eslint.config.mjs index c21edec..52a5848 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -22,7 +22,7 @@ export default [ }, }, rules: { - 'no-unused-vars': ['warn', { 'caughtErrorsIgnorePattern': '^ignore' }], + 'no-unused-vars': ['warn', { caughtErrorsIgnorePattern: '^ignore' }], }, }, ] diff --git a/lib/reader.js b/lib/reader.js index a6bb385..2cae6c0 100644 --- a/lib/reader.js +++ b/lib/reader.js @@ -35,10 +35,7 @@ class Reader { } // when loaded with require('haraka-config') - if ( - __dirname.split(path.sep).slice(-3).toString() === - 'node_modules,haraka-config,lib' - ) { + if (__dirname.split(path.sep).slice(-3).toString() === 'node_modules,haraka-config,lib') { config_dir_candidates = [ path.join(__dirname, '..', '..', '..', 'config'), // haraka/Haraka/* path.join(__dirname, '..', '..', '..'), // npm packaged modules @@ -233,8 +230,7 @@ class Reader { if (this._config_cache[cache_key]) { for (const ck in this._config_cache[cache_key]) { - if (ck.substr(0, 1) === '!') - delete this._config_cache[path.join(cp, ck.substr(1))] + if (ck.substr(0, 1) === '!') delete this._config_cache[path.join(cp, ck.substr(1))] } } diff --git a/lib/readers/flat.js b/lib/readers/flat.js index cc689c3..d09df87 100644 --- a/lib/readers/flat.js +++ b/lib/readers/flat.js @@ -3,17 +3,11 @@ const regex = require('../regex') exports.load = (...args) => { - return this.parseValue( - ...args, - require('node:fs').readFileSync(args[0], 'UTF-8'), - ) + return this.parseValue(...args, require('node:fs').readFileSync(args[0], 'UTF-8')) } exports.loadPromise = async (...args) => { - return this.parseValue( - ...args, - await require('node:fs/promises').readFile(args[0], 'UTF-8'), - ) + return this.parseValue(...args, await require('node:fs/promises').readFile(args[0], 'UTF-8')) } exports.parseValue = (name, type, options, data) => { diff --git a/lib/readers/ini.js b/lib/readers/ini.js index 6d8f2a6..eea71a7 100644 --- a/lib/readers/ini.js +++ b/lib/readers/ini.js @@ -3,17 +3,11 @@ const regex = require('../regex') exports.load = (...args) => { - return this.parseIni( - ...args, - require('node:fs').readFileSync(args[0], 'UTF-8'), - ) + return this.parseIni(...args, require('node:fs').readFileSync(args[0], 'UTF-8')) } exports.loadPromise = async (...args) => { - return this.parseIni( - ...args, - await require('node:fs/promises').readFile(args[0], 'UTF-8'), - ) + return this.parseIni(...args, await require('node:fs/promises').readFile(args[0], 'UTF-8')) } exports.parseIni = (name, options = {}, data) => { @@ -61,10 +55,7 @@ exports.parseIni = (name, options = {}, data) => { const setter = this.getSetter(current_sect, regex.is_array.test(keyName)) - if ( - exports.isDeclaredBoolean(`${current_sect_name}.${keyName}`) || - exports.isDeclaredBoolean(`*.${keyName}`) - ) { + if (exports.isDeclaredBoolean(`${current_sect_name}.${keyName}`) || exports.isDeclaredBoolean(`*.${keyName}`)) { current_sect[keyName] = regex.is_truth.test(keyVal) } else if (regex.is_integer.test(keyVal)) { setter(keyName, parseInt(keyVal, 10)) @@ -114,8 +105,7 @@ exports.init_booleans = (options, result) => { let section = m[1] || 'main' let key = m[2] - const bool_default = - section[0] === '+' ? true : key[0] === '+' ? true : false + const bool_default = section[0] === '+' ? true : key[0] === '+' ? true : false if (section.match(/^(-|\+)/)) section = section.substr(1) if (key.match(/^(-|\+)/)) key = key.substr(1) diff --git a/lib/readers/yaml.js b/lib/readers/yaml.js index 35a88dd..98b690b 100644 --- a/lib/readers/yaml.js +++ b/lib/readers/yaml.js @@ -1,13 +1,13 @@ 'use strict' -const yaml = require('js-yaml') +const yaml = require('yaml') exports.load = (name) => { - return yaml.load(require('node:fs').readFileSync(name, 'UTF-8')) + return yaml.parse(require('node:fs').readFileSync(name, 'UTF-8')) } exports.loadPromise = async (name) => { - return yaml.load(await require('node:fs/promises').readFile(name, 'UTF-8')) + return yaml.parse(await require('node:fs/promises').readFile(name, 'UTF-8')) } exports.empty = () => { diff --git a/lib/watch.js b/lib/watch.js index 061482a..0b67266 100644 --- a/lib/watch.js +++ b/lib/watch.js @@ -18,11 +18,7 @@ Watch.ensure_enoent_timer = (reader) => { delete enoent.files[file] const args = reader._read_args[file] reader.load_config(file, args.type, args.options, args.cb) - watchers[file] = fs.watch( - file, - { persistent: false }, - Watch.onEvent(reader, file, args), - ) + watchers[file] = fs.watch(file, { persistent: false }, Watch.onEvent(reader, file, args)) }) } }, 60 * 1000) @@ -38,11 +34,7 @@ Watch.file = (reader, name, type, cb, options) => { if (watchers[name] || (options && options.no_watch)) return try { - watchers[name] = fs.watch( - name, - { persistent: false }, - Watch.onEvent(reader, name, { type, options, cb }), - ) + watchers[name] = fs.watch(name, { persistent: false }, Watch.onEvent(reader, name, { type, options, cb })) } catch (e) { if (e.code === 'ENOENT') { // ignore error when ENOENT @@ -126,11 +118,7 @@ Watch.onEvent = (reader, name, args) => { // After a rename event, re-watch the file watchers[name].close() try { - watchers[name] = fs.watch( - name, - { persistent: false }, - Watch.onEvent(reader, name, args), - ) + watchers[name] = fs.watch(name, { persistent: false }, Watch.onEvent(reader, name, args)) } catch (e) { if (e.code === 'ENOENT') { enoent.files[name] = true diff --git a/package.json b/package.json index 1f1bbf9..7f1c7e4 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "name": "haraka-config", "license": "MIT", "description": "Haraka's config file loader", - "version": "1.4.2", + "version": "1.5.0", "homepage": "https://github.com/haraka/haraka-config", "repository": { "type": "git", @@ -15,16 +15,16 @@ "CHANGELOG.md" ], "engines": { - "node": ">=16" + "node": ">=20" }, "dependencies": { - "js-yaml": "^4.1.0" + "yaml": "^2.8.2" }, "optionalDependencies": { "hjson": "^3.2.2" }, "devDependencies": { - "@haraka/eslint-config": "^2.0.2" + "@haraka/eslint-config": "^2.0.3" }, "bugs": { "url": "https://github.com/haraka/haraka-config/issues" @@ -35,8 +35,13 @@ "lint:fix": "npx eslint *.js lib test test/*/*.js --fix", "prettier": "npx prettier . --check", "prettier:fix": "npx prettier . --write --log-level=warn", - "test": "npx mocha@10 test test/readers", + "test": "node --test test/*.js test/readers/*.js", "versions": "npx dependency-version-checker check", "versions:fix": "npx dependency-version-checker update && npm run prettier:fix" + }, + "prettier": { + "singleQuote": true, + "printWidth": 120, + "semi": false } } diff --git a/test/config.js b/test/config.js index f94b442..b3d7ed9 100644 --- a/test/config.js +++ b/test/config.js @@ -1,6 +1,6 @@ const assert = require('node:assert') -// const { beforeEach, describe, it } = require('node:test') -const fs = require('node:fs') +const { beforeEach, describe, it } = require('node:test') +const fs = require('node:fs/promises') const os = require('node:os') const path = require('node:path') @@ -16,13 +16,12 @@ function clearRequireCache() { delete require.cache[`${path.resolve(__dirname, '..', 'lib', 'reader')}.js`] } -function testSetup(done) { +function testSetup() { process.env.NODE_ENV = 'test' process.env.HARAKA = '' process.env.WITHOUT_CONFIG_CACHE = '1' clearRequireCache() this.config = require('../config') - done() } describe('config', function () { @@ -59,117 +58,61 @@ describe('config', function () { beforeEach(testSetup) it('name', function () { - assert.deepEqual(this.config.arrange_args(['test.ini']), [ - 'test.ini', - 'ini', - undefined, - undefined, - ]) + assert.deepEqual(this.config.arrange_args(['test.ini']), ['test.ini', 'ini', undefined, undefined]) }) it('name, type', function () { - assert.deepEqual(this.config.arrange_args(['test.ini', 'ini']), [ - 'test.ini', - 'ini', - undefined, - undefined, - ]) + assert.deepEqual(this.config.arrange_args(['test.ini', 'ini']), ['test.ini', 'ini', undefined, undefined]) }) it('name, callback', function () { - assert.deepEqual(this.config.arrange_args(['test.ini', cb]), [ - 'test.ini', - 'ini', - cb, - undefined, - ]) + assert.deepEqual(this.config.arrange_args(['test.ini', cb]), ['test.ini', 'ini', cb, undefined]) }) it('name, callback, options', function () { - assert.deepEqual(this.config.arrange_args(['test.ini', cb, opts]), [ - 'test.ini', - 'ini', - cb, - opts, - ]) + assert.deepEqual(this.config.arrange_args(['test.ini', cb, opts]), ['test.ini', 'ini', cb, opts]) }) it('name, options', function () { - assert.deepEqual(this.config.arrange_args(['test.ini', opts]), [ - 'test.ini', - 'ini', - undefined, - opts, - ]) + assert.deepEqual(this.config.arrange_args(['test.ini', opts]), ['test.ini', 'ini', undefined, opts]) }) it('name, type, callback', function () { - assert.deepEqual(this.config.arrange_args(['test.ini', 'ini', cb]), [ - 'test.ini', - 'ini', - cb, - undefined, - ]) + assert.deepEqual(this.config.arrange_args(['test.ini', 'ini', cb]), ['test.ini', 'ini', cb, undefined]) }) it('name, type, options', function () { - assert.deepEqual(this.config.arrange_args(['test.ini', 'ini', opts]), [ - 'test.ini', - 'ini', - undefined, - opts, - ]) + assert.deepEqual(this.config.arrange_args(['test.ini', 'ini', opts]), ['test.ini', 'ini', undefined, opts]) }) it('name, type, callback, options', function () { - assert.deepEqual( - this.config.arrange_args(['test.ini', 'ini', cb, opts]), - ['test.ini', 'ini', cb, opts], - ) + assert.deepEqual(this.config.arrange_args(['test.ini', 'ini', cb, opts]), ['test.ini', 'ini', cb, opts]) }) it('name, list type, callback, options', function () { - assert.deepEqual( - this.config.arrange_args(['test.ini', 'list', cb, opts]), - ['test.ini', 'list', cb, opts], - ) + assert.deepEqual(this.config.arrange_args(['test.ini', 'list', cb, opts]), ['test.ini', 'list', cb, opts]) }) it('name, binary type, callback, options', function () { - assert.deepEqual( - this.config.arrange_args(['test.ini', 'binary', cb, opts]), - ['test.ini', 'binary', cb, opts], - ) + assert.deepEqual(this.config.arrange_args(['test.ini', 'binary', cb, opts]), ['test.ini', 'binary', cb, opts]) }) it('name, value type, callback, options', function () { - assert.deepEqual( - this.config.arrange_args(['test.ini', 'value', cb, opts]), - ['test.ini', 'value', cb, opts], - ) + assert.deepEqual(this.config.arrange_args(['test.ini', 'value', cb, opts]), ['test.ini', 'value', cb, opts]) }) it('name, hjson type, callback, options', function () { - assert.deepEqual( - this.config.arrange_args(['test.ini', 'hjson', cb, opts]), - ['test.ini', 'hjson', cb, opts], - ) + assert.deepEqual(this.config.arrange_args(['test.ini', 'hjson', cb, opts]), ['test.ini', 'hjson', cb, opts]) }) // config.get('name', type, cb, options); it('name, json type, callback, options', function () { - assert.deepEqual( - this.config.arrange_args(['test.ini', 'json', cb, opts]), - ['test.ini', 'json', cb, opts], - ) + assert.deepEqual(this.config.arrange_args(['test.ini', 'json', cb, opts]), ['test.ini', 'json', cb, opts]) }) // config.get('name', type, cb, options); it('name, data type, callback, options', function () { - assert.deepEqual( - this.config.arrange_args(['test.ini', 'data', cb, opts]), - ['test.ini', 'data', cb, opts], - ) + assert.deepEqual(this.config.arrange_args(['test.ini', 'data', cb, opts]), ['test.ini', 'data', cb, opts]) }) }) }) @@ -321,22 +264,11 @@ describe('get', function () { }) it('test.flat, type=list', function () { - _test_get('test.list', 'list', null, null, [ - 'line1', - 'line2', - 'line3', - 'line5', - ]) + _test_get('test.list', 'list', null, null, ['line1', 'line2', 'line3', 'line5']) }) it('test.flat, type=data', function () { - _test_get('test.data', 'data', null, null, [ - 'line1', - 'line2', - 'line3', - '', - 'line5', - ]) + _test_get('test.data', 'data', null, null, ['line1', 'line2', 'line3', '', 'line5']) }) it('test.hjson, type=', function () { @@ -402,10 +334,7 @@ describe('merged', function () { }) it('after_merge', function () { - const lc = this.config.module_config( - path.join('test', 'default'), - path.join('test', 'override'), - ) + const lc = this.config.module_config(path.join('test', 'default'), path.join('test', 'override')) assert.deepEqual(lc.get('test.ini'), { main: {}, defaults: { one: 'three', two: 'four' }, @@ -413,10 +342,7 @@ describe('merged', function () { }) it('flat overridden', function () { - const lc = this.config.module_config( - path.join('test', 'default'), - path.join('test', 'override'), - ) + const lc = this.config.module_config(path.join('test', 'default'), path.join('test', 'override')) assert.equal(lc.get('test.flat'), 'flatoverrode') }) }) @@ -453,65 +379,64 @@ describe('getInt', function () { const tmpFile = path.resolve('test', 'config', 'dir', '4.ext') describe('getDir', function () { - beforeEach(function (done) { + beforeEach(async () => { process.env.NODE_ENV = 'test' process.env.HARAKA = '' process.env.WITHOUT_CONFIG_CACHE = '1' clearRequireCache() this.config = require('../config') - fs.unlink(tmpFile, () => done()) - }) - - it('loads all files in dir', function (done) { - this.config.getDir('dir', { type: 'binary' }, (err, files) => { - if (err) console.error(err) - assert.ifError(err) - assert.equal(err, null) - assert.equal(files.length, 4) - assert.equal(files[0].data, `contents1${os.EOL}`) - assert.equal(files[2].data, `contents3${os.EOL}`) - done() - }) + await fs.unlink(tmpFile).catch(() => {}) + }) + + it('loads all files in dir', async () => { + const files = await this.config.getDir('dir', { type: 'binary' }) + assert.equal(files.length, 4) + assert.equal(files[0].data, `contents1${os.EOL}`) + assert.equal(files[2].data, `contents3${os.EOL}`) }) - it('errs on invalid dir', function (done) { - this.config.getDir('dirInvalid', { type: 'binary' }, (err) => { + it('errs on invalid dir', async () => { + try { + await this.config.getDir('dirInvalid', { type: 'binary' }) + assert.fail('expected error') + } catch (err) { assert.equal(err.code, 'ENOENT') - done() - }) + } }) - it('reloads when file in dir is touched', function (done) { - this.timeout(3500) - - // due to differences in fs.watch, this test is unreliable on Mac OS X - // if (/darwin/.test(process.platform)) return done() - - let callCount = 0 - - const getDir = () => { - const opts2 = { type: 'binary', watchCb: getDir } - this.config.getDir('dir', opts2, (err, files) => { - // console.log('Loading: test/config/dir'); - if (err) console.error(err) - callCount++ - if (callCount === 1) { - assert.equal(err, null) - assert.equal(files.length, 4) - assert.equal(files[0].data, `contents1${os.EOL}`) - assert.equal(files[2].data, `contents3${os.EOL}`) - fs.writeFile(tmpFile, 'contents4\n', (err2) => { - assert.equal(err2, null) - // console.log('file touched, waiting for callback'); - }) - } else if (callCount === 2) { - assert.equal(files[3].data, 'contents4\n') - fs.unlink(tmpFile, () => {}) - done() + it('reloads when file in dir is touched', { timeout: 5000 }, async (t) => { + // due to differences in fs.watch, this test is unreliable on macOS with Node < 24 + const nodeMajorVersion = parseInt(process.versions.node.split('.')[0]) + if (/darwin/.test(process.platform) && nodeMajorVersion < 24) return + + await t.test('waits for watch event', async () => { + return new Promise((resolve) => { + let callCount = 0 + + const getDir = async () => { + try { + const opts2 = { type: 'binary', watchCb: getDir } + const files = await this.config.getDir('dir', opts2) + callCount++ + if (callCount === 1) { + assert.equal(files.length, 4) + assert.equal(files[0].data, `contents1${os.EOL}`) + assert.equal(files[2].data, `contents3${os.EOL}`) + await fs.writeFile(tmpFile, 'contents4\n') + } else if (callCount === 2) { + assert.equal(files[3].data, 'contents4\n') + await fs.unlink(tmpFile) + resolve() + } else { + console.log('unexpected call count: ', callCount) + } + } catch (err) { + console.error(err) + } } + getDir() }) - } - getDir() + }) }) }) @@ -542,9 +467,6 @@ describe('jsonOverrides', function () { it('with smtpgreeting override', function () { process.env.WITHOUT_CONFIG_CACHE = '' this.config.get('main.json') - assert.deepEqual(this.config.get('smtpgreeting', 'list'), [ - 'this is line one', - 'this is line two', - ]) + assert.deepEqual(this.config.get('smtpgreeting', 'list'), ['this is line one', 'this is line two']) }) }) diff --git a/test/reader.js b/test/reader.js index bfeeab0..d1ed3f7 100644 --- a/test/reader.js +++ b/test/reader.js @@ -1,14 +1,14 @@ 'use strict' const assert = require('node:assert') +const { beforeEach, describe, it } = require('node:test') const path = require('node:path') describe('reader', function () { - beforeEach(function (done) { + beforeEach(function () { process.env.NODE_ENV === 'test' this.cfreader = require('../lib/reader') this.opts = { booleans: ['main.bool_true', 'main.bool_false'] } - done() }) describe('load_config', function () { @@ -96,11 +96,7 @@ describe('reader', function () { }) it('opts', function () { - const r = this.cfreader.load_config( - 'test/config/test.ini', - 'ini', - this.opts, - ) + const r = this.cfreader.load_config('test/config/test.ini', 'ini', this.opts) assert.strictEqual(r.main.bool_true, true) assert.strictEqual(r.main.bool_false, false) assert.strictEqual(r.main.str_true, 'true') @@ -119,12 +115,7 @@ describe('reader', function () { it('sect1, opts, w/defaults', function () { const r = this.cfreader.load_config('test/config/test.ini', 'ini', { - booleans: [ - '+sect1.bool_true', - '-sect1.bool_false', - '+sect1.bool_true_default', - 'sect1.-bool_false_default', - ], + booleans: ['+sect1.bool_true', '-sect1.bool_false', '+sect1.bool_true_default', 'sect1.-bool_false_default'], }) assert.strictEqual(r.sect1.bool_true, true) assert.strictEqual(r.sect1.bool_false, false) @@ -151,18 +142,12 @@ describe('reader', function () { it('empty value', function () { const r = this.cfreader.load_config('test/config/test.ini') - assert.deepEqual( - { first: undefined, second: undefined }, - r.empty_values, - ) + assert.deepEqual({ first: undefined, second: undefined }, r.empty_values) }) it('array', function () { const r = this.cfreader.load_config('test/config/test.ini') - assert.deepEqual( - ['first_host', 'second_host', 'third_host'], - r.array_test.hostlist, - ) + assert.deepEqual(['first_host', 'second_host', 'third_host'], r.array_test.hostlist) assert.deepEqual([123, 456, 789], r.array_test.intlist) }) }) @@ -264,10 +249,7 @@ describe('reader', function () { }) it('null for binary file', function () { - const result = this.cfreader.load_config( - 'test/config/non-existent.bin', - 'binary', - ) + const result = this.cfreader.load_config('test/config/non-existent.bin', 'binary') assert.equal(result, null) }) @@ -298,17 +280,11 @@ describe('reader', function () { }) it('one option is name + serialized opts', function () { - assert.equal( - this.cfreader.get_cache_key('test', { foo: 'bar' }), - 'test{"foo":"bar"}', - ) + assert.equal(this.cfreader.get_cache_key('test', { foo: 'bar' }), 'test{"foo":"bar"}') }) it('two options are returned predictably', function () { - assert.equal( - this.cfreader.get_cache_key('test', { opt1: 'foo', opt2: 'bar' }), - 'test{"opt1":"foo","opt2":"bar"}', - ) + assert.equal(this.cfreader.get_cache_key('test', { opt1: 'foo', opt2: 'bar' }), 'test{"opt1":"foo","opt2":"bar"}') }) }) @@ -320,10 +296,7 @@ describe('reader', function () { describe('overrides', function () { it('missing hjson loads yaml instead', function () { - assert.deepEqual( - this.cfreader.load_config('test/config/override2.hjson'), - { hasDifferent: { value: false } }, - ) + assert.deepEqual(this.cfreader.load_config('test/config/override2.hjson'), { hasDifferent: { value: false } }) }) it('missing json loads yaml instead', function () { @@ -337,10 +310,7 @@ describe('reader', function () { it('Haraka runtime (env.HARAKA=*)', function () { process.env.HARAKA = '/etc/' this.cfreader.get_path_to_config_dir() - assert.ok( - /etc.config$/.test(this.cfreader.config_path), - this.cfreader.config_path, - ) + assert.ok(/etc.config$/.test(this.cfreader.config_path), this.cfreader.config_path) delete process.env.HARAKA }) @@ -348,10 +318,7 @@ describe('reader', function () { delete process.env.HARAKA process.env.NODE_ENV = 'test' this.cfreader.get_path_to_config_dir() - assert.ok( - /haraka-config.test.config$/.test(this.cfreader.config_path), - this.cfreader.config_path, - ) + assert.ok(/haraka-config.test.config$/.test(this.cfreader.config_path), this.cfreader.config_path) delete process.env.NODE_ENV }) @@ -359,10 +326,7 @@ describe('reader', function () { delete process.env.HARAKA delete process.env.NODE_ENV this.cfreader.get_path_to_config_dir() - assert.ok( - /haraka-config$/.test(this.cfreader.config_path), - this.cfreader.config_path, - ) + assert.ok(/haraka-config$/.test(this.cfreader.config_path), this.cfreader.config_path) }) }) }) diff --git a/test/readers/binary.js b/test/readers/binary.js index aff676e..f8b5fde 100644 --- a/test/readers/binary.js +++ b/test/readers/binary.js @@ -1,10 +1,10 @@ -const assert = require('assert') -const fs = require('fs') -const path = require('path') +const assert = require('node:assert') +const { beforeEach, describe, it } = require('node:test') +const fs = require('node:fs') +const path = require('node:path') -beforeEach(function (done) { +beforeEach(function () { this.bin = require('../../lib/readers/binary') - done() }) describe('binary', function () { diff --git a/test/readers/flat.js b/test/readers/flat.js index 78f8d2f..ce9f0e2 100644 --- a/test/readers/flat.js +++ b/test/readers/flat.js @@ -1,8 +1,8 @@ -const assert = require('assert') +const assert = require('node:assert') +const { beforeEach, describe, it } = require('node:test') -beforeEach(function (done) { +beforeEach(function () { this.flat = require('../../lib/readers/flat') - done() }) describe('flat', function () { diff --git a/test/readers/hjson.js b/test/readers/hjson.js index 4682428..8e35d81 100644 --- a/test/readers/hjson.js +++ b/test/readers/hjson.js @@ -1,9 +1,9 @@ -const assert = require('assert') -const path = require('path') +const assert = require('node:assert') +const { beforeEach, describe, it } = require('node:test') +const path = require('node:path') -beforeEach(function (done) { +beforeEach(function () { this.hjson = require('../../lib/readers/hjson') - done() }) describe('hjson', function () { diff --git a/test/readers/ini.js b/test/readers/ini.js index e31e057..7bbb51e 100644 --- a/test/readers/ini.js +++ b/test/readers/ini.js @@ -1,11 +1,11 @@ -const assert = require('assert') +const assert = require('node:assert') +const { beforeEach, describe, it } = require('node:test') -beforeEach(function (done) { +beforeEach(function () { this.ini = require('../../lib/readers/ini') this.opts = { booleans: ['main.bool_true', 'main.bool_false'], } - done() }) describe('ini', function () { @@ -57,12 +57,7 @@ describe('ini', function () { it('sect1, opts, w/defaults', function () { const r = this.ini.load('test/config/test.ini', { - booleans: [ - '+sect1.bool_true', - '-sect1.bool_false', - '+sect1.bool_true_default', - 'sect1.-bool_false_default', - ], + booleans: ['+sect1.bool_true', '-sect1.bool_false', '+sect1.bool_true_default', 'sect1.-bool_false_default'], }) assert.strictEqual(r.sect1.bool_true, true) assert.strictEqual(r.sect1.bool_false, false) diff --git a/test/readers/json.js b/test/readers/json.js index 988fd1e..5d7f87e 100644 --- a/test/readers/json.js +++ b/test/readers/json.js @@ -1,8 +1,8 @@ -const assert = require('assert') +const assert = require('node:assert') +const { beforeEach, describe, it } = require('node:test') -beforeEach(function (done) { +beforeEach(function () { this.json = require('../../lib/readers/json') - done() }) describe('json', function () { diff --git a/test/readers/yaml.js b/test/readers/yaml.js index f2a2d83..176e256 100644 --- a/test/readers/yaml.js +++ b/test/readers/yaml.js @@ -1,8 +1,8 @@ -const assert = require('assert') +const assert = require('node:assert') +const { beforeEach, describe, it } = require('node:test') -beforeEach(function (done) { +beforeEach(function () { this.yaml = require('../../lib/readers/yaml') - done() }) describe('yaml', function () { diff --git a/test/regex.js b/test/regex.js index f095af0..fb83016 100644 --- a/test/regex.js +++ b/test/regex.js @@ -1,4 +1,5 @@ const assert = require('node:assert') +const { describe, it } = require('node:test') const regex = require('../lib/regex') diff --git a/test/watch.js b/test/watch.js new file mode 100644 index 0000000..b1f420a --- /dev/null +++ b/test/watch.js @@ -0,0 +1,252 @@ +'use strict' + +const assert = require('node:assert') +const { afterEach, beforeEach, describe, it } = require('node:test') +const fs = require('node:fs') +const path = require('node:path') + +function loadWatch() { + delete require.cache[require.resolve('../lib/watch')] + return require('../lib/watch') +} + +describe('watch', function () { + let fsWatch + let fsStat + let setTimeoutFn + let clearTimeoutFn + let setIntervalFn + let consoleError + let consoleLog + + beforeEach(function () { + fsWatch = fs.watch + fsStat = fs.stat + setTimeoutFn = global.setTimeout + clearTimeoutFn = global.clearTimeout + setIntervalFn = global.setInterval + consoleError = console.error + consoleLog = console.log + }) + + afterEach(function () { + fs.watch = fsWatch + fs.stat = fsStat + global.setTimeout = setTimeoutFn + global.clearTimeout = clearTimeoutFn + global.setInterval = setIntervalFn + console.error = consoleError + console.log = consoleLog + delete require.cache[require.resolve('../lib/watch')] + }) + + it('file skips no_watch and avoids duplicate watchers', function () { + const Watch = loadWatch() + let watchCalls = 0 + + fs.watch = () => { + watchCalls++ + return { close() {}, unref() {} } + } + + Watch.file({}, 'test/config/test.ini', 'ini', null, { no_watch: true }) + Watch.file({}, 'test/config/test.ini', 'ini') + Watch.file({}, 'test/config/test.ini', 'ini') + + assert.equal(watchCalls, 1) + }) + + it('file handles ENOENT and recovers via stat timer', function () { + const Watch = loadWatch() + const name = path.join('test', 'config', 'missing-watch.ini') + const reader = { + _read_args: { + [name]: { type: 'ini', options: { booleans: ['main.test'] }, cb() {} }, + }, + load_config_calls: 0, + load_config() { + this.load_config_calls++ + }, + } + + let watchCalls = 0 + let timerFn + let intervalUnrefCalls = 0 + + fs.watch = () => { + watchCalls++ + if (watchCalls === 1) { + const err = new Error('missing') + err.code = 'ENOENT' + throw err + } + return { close() {}, unref() {} } + } + + fs.stat = (file, cb) => { + assert.equal(file, name) + cb(null, {}) + } + + global.setInterval = (fn) => { + timerFn = fn + return { + unref() { + intervalUnrefCalls++ + }, + } + } + + Watch.file(reader, name, 'ini', reader._read_args[name].cb, { + booleans: ['main.test'], + }) + Watch.file(reader, `${name}.again`, 'ini', null, null) + + assert.equal(typeof timerFn, 'function') + assert.equal(intervalUnrefCalls, 1) + + timerFn() + + assert.equal(reader.load_config_calls, 1) + assert.equal(watchCalls, 3) + }) + + it('file logs non-ENOENT watch errors', function () { + const Watch = loadWatch() + const errors = [] + + fs.watch = () => { + const err = new Error('denied') + err.code = 'EACCES' + throw err + } + console.error = (msg) => errors.push(msg) + + Watch.file({}, 'test/config/test.ini', 'ini') + + assert.equal(errors.length, 1) + assert.match(errors[0], /Error watching config file:/) + }) + + it('onEvent reloads and re-watches on rename', function () { + const Watch = loadWatch() + const name = path.join('test', 'config', 'test.ini') + const reader = { + load_config_calls: 0, + load_config() { + this.load_config_calls++ + }, + } + const args = { + type: 'ini', + options: {}, + cb_calls: 0, + cb() { + this.cb_calls++ + }, + } + + const watcher = { + closed: 0, + close() { + this.closed++ + }, + unref() {}, + } + let watchCalls = 0 + let watchListener + + fs.watch = (file, opts, listener) => { + watchCalls++ + watchListener = listener + return watcher + } + + global.setTimeout = (fn) => { + fn() + return 1 + } + global.clearTimeout = () => {} + console.log = () => {} + + Watch.file(reader, name, 'ini', args.cb.bind(args), args.options) + watchListener('rename') + + assert.equal(reader.load_config_calls, 1) + assert.equal(args.cb_calls, 1) + assert.equal(watcher.closed, 1) + assert.equal(watchCalls, 2) + }) + + it('dir and dir2 callbacks reload and invoke watchCb', function () { + const Watch = loadWatch() + const cfgPath = path.resolve('test/config') + const dirPath = path.resolve('test/config/dir') + const filename = 'test.ini' + const fullPath = path.join(cfgPath, filename) + + const reader = { + config_path: cfgPath, + _read_args: { + [fullPath]: { + type: 'ini', + options: {}, + cb_calls: 0, + cb() { + this.cb_calls++ + }, + }, + [dirPath]: { + opts: { + watchCb_calls: 0, + watchCb() { + this.watchCb_calls++ + }, + }, + }, + }, + load_config_calls: 0, + load_config() { + this.load_config_calls++ + }, + } + + const watchCalls = [] + const watchers = [] + + fs.watch = (target, opts, listener) => { + watchCalls.push({ target, opts, listener }) + const w = { + unref_calls: 0, + close() {}, + unref() { + this.unref_calls++ + }, + } + watchers.push(w) + return w + } + + global.setTimeout = (fn) => { + fn() + return 1 + } + global.clearTimeout = () => {} + console.log = () => {} + + Watch.dir(reader) + watchCalls[0].listener('change') + watchCalls[0].listener('change', 'nope.ini') + watchCalls[0].listener('change', filename) + + Watch.dir2(reader, dirPath) + watchCalls[1].listener('change', '1.ext') + + assert.equal(reader.load_config_calls, 1) + assert.equal(reader._read_args[fullPath].cb_calls, 1) + assert.equal(reader._read_args[dirPath].opts.watchCb_calls, 1) + assert.equal(watchCalls[1].opts.persistent, false) + assert.equal(watchCalls[1].opts.recursive, /win|darwin/.test(process.platform)) + assert.equal(watchers[1].unref_calls, 1) + }) +})