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)
+ })
+})