adapt-authoring-config 1.1.5 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/tests.yml +15 -0
- package/lib/ConfigModule.js +6 -29
- package/package.json +5 -2
- package/tests/configModule.spec.js +123 -50
- package/tests/configUtils.spec.js +0 -36
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
on: push
|
|
3
|
+
jobs:
|
|
4
|
+
default:
|
|
5
|
+
runs-on: ubuntu-latest
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
steps:
|
|
9
|
+
- uses: actions/checkout@master
|
|
10
|
+
- uses: actions/setup-node@master
|
|
11
|
+
with:
|
|
12
|
+
node-version: 'lts/*'
|
|
13
|
+
cache: 'npm'
|
|
14
|
+
- run: npm ci
|
|
15
|
+
- run: npm test
|
package/lib/ConfigModule.js
CHANGED
|
@@ -21,11 +21,6 @@ class ConfigModule extends AbstractModule {
|
|
|
21
21
|
* @type {String}
|
|
22
22
|
*/
|
|
23
23
|
this.configFilePath = path.join(this.app.rootDir, 'conf', `${process.env.NODE_ENV}.config.js`)
|
|
24
|
-
/**
|
|
25
|
-
* The keys for all attributes which can be modified during runtime
|
|
26
|
-
* @type {Array<String>}
|
|
27
|
-
*/
|
|
28
|
-
this.mutableAttributes = []
|
|
29
24
|
/**
|
|
30
25
|
* The keys for all attributes marked as public
|
|
31
26
|
* @type {Array<String>}
|
|
@@ -61,7 +56,7 @@ class ConfigModule extends AbstractModule {
|
|
|
61
56
|
const [auth, server] = await this.app.waitForModule('auth', 'server')
|
|
62
57
|
const router = server.api.createChildRouter('config', [{
|
|
63
58
|
route: '/',
|
|
64
|
-
handlers: { get: (
|
|
59
|
+
handlers: { get: (_req, res) => res.json(this.getPublicConfig()) },
|
|
65
60
|
meta: {
|
|
66
61
|
get: {
|
|
67
62
|
summary: 'Retrieve public config data',
|
|
@@ -86,7 +81,7 @@ class ConfigModule extends AbstractModule {
|
|
|
86
81
|
try { // try to parse to allow for non-string values
|
|
87
82
|
val = JSON.parse(val)
|
|
88
83
|
} catch {} // ignore errors
|
|
89
|
-
this.
|
|
84
|
+
this._config[this.envVarToConfigKey(key)] = val
|
|
90
85
|
})
|
|
91
86
|
}
|
|
92
87
|
|
|
@@ -126,7 +121,7 @@ class ConfigModule extends AbstractModule {
|
|
|
126
121
|
}
|
|
127
122
|
Object.entries(config).forEach(([name, c]) => {
|
|
128
123
|
Object.entries(c).forEach(([key, val]) => {
|
|
129
|
-
this.
|
|
124
|
+
this._config[`${name}.${key}`] = val
|
|
130
125
|
})
|
|
131
126
|
})
|
|
132
127
|
}
|
|
@@ -178,7 +173,6 @@ class ConfigModule extends AbstractModule {
|
|
|
178
173
|
}
|
|
179
174
|
// validate user config data
|
|
180
175
|
let data = Object.entries(schema.raw.properties).reduce((m, [k, v]) => {
|
|
181
|
-
if (v?._adapt?.isMutable) this.mutableAttributes.push(`${pkg.name}.${k}`)
|
|
182
176
|
if (v?._adapt?.isPublic) this.publicAttributes.push(`${pkg.name}.${k}`)
|
|
183
177
|
return { ...m, [k]: this.get(`${pkg.name}.${k}`) }
|
|
184
178
|
}, {})
|
|
@@ -189,7 +183,7 @@ class ConfigModule extends AbstractModule {
|
|
|
189
183
|
throw e
|
|
190
184
|
}
|
|
191
185
|
// apply validated config settings
|
|
192
|
-
Object.entries(data).forEach(([key, val]) => this.
|
|
186
|
+
Object.entries(data).forEach(([key, val]) => { this._config[`${pkg.name}.${key}`] = val })
|
|
193
187
|
}
|
|
194
188
|
|
|
195
189
|
/**
|
|
@@ -210,30 +204,13 @@ class ConfigModule extends AbstractModule {
|
|
|
210
204
|
return this._config[attr]
|
|
211
205
|
}
|
|
212
206
|
|
|
213
|
-
/**
|
|
214
|
-
* Stores a value for the passed attribute
|
|
215
|
-
* @param {String} attr Attribute key name
|
|
216
|
-
* @param {*} val Value to set
|
|
217
|
-
* @param {objeect} options Custom options
|
|
218
|
-
* @param {objeect} options.force Whether to force an update
|
|
219
|
-
*/
|
|
220
|
-
set (attr, val, options = {}) {
|
|
221
|
-
if (this.has(attr) && !this.mutableAttributes.includes(attr) && options.force !== true) {
|
|
222
|
-
return
|
|
223
|
-
}
|
|
224
|
-
this._config[attr] = val
|
|
225
|
-
}
|
|
226
|
-
|
|
227
207
|
/**
|
|
228
208
|
* Retrieves all config options marked as 'public'
|
|
229
|
-
* @param {Boolean} isMutable Whether options should also be mutable
|
|
230
209
|
* @return {Object}
|
|
231
210
|
*/
|
|
232
|
-
getPublicConfig (
|
|
211
|
+
getPublicConfig () {
|
|
233
212
|
return this.publicAttributes.reduce((m, a) => {
|
|
234
|
-
|
|
235
|
-
m[a] = this.get(a)
|
|
236
|
-
}
|
|
213
|
+
m[a] = this.get(a)
|
|
237
214
|
return m
|
|
238
215
|
}, {})
|
|
239
216
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adapt-authoring-config",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "A configuration module for the Adapt authoring tool.",
|
|
5
5
|
"homepage": "https://github.com/adapt-security/adapt-authoring-config",
|
|
6
6
|
"license": "GPL-3.0",
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"bin": {
|
|
10
10
|
"at-confgen": "./bin/confgen.js"
|
|
11
11
|
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "node --test tests/*.spec.js"
|
|
14
|
+
},
|
|
12
15
|
"repository": "github:adapt-security/adapt-authoring-config",
|
|
13
16
|
"dependencies": {
|
|
14
17
|
"adapt-authoring-core": "^1.7.0",
|
|
@@ -16,7 +19,7 @@
|
|
|
16
19
|
"glob": "^13.0.0"
|
|
17
20
|
},
|
|
18
21
|
"peerDependencies": {
|
|
19
|
-
"adapt-authoring-auth": "^1.0.
|
|
22
|
+
"adapt-authoring-auth": "^1.0.7",
|
|
20
23
|
"adapt-authoring-errors": "^1.1.2",
|
|
21
24
|
"adapt-authoring-jsonschema": "^1.2.0",
|
|
22
25
|
"adapt-authoring-server": "^1.2.1"
|
|
@@ -1,63 +1,136 @@
|
|
|
1
|
-
|
|
1
|
+
import { describe, it, before } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import ConfigModule from '../lib/ConfigModule.js'
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// const should = require('should')
|
|
5
|
+
describe('ConfigModule', () => {
|
|
6
|
+
let instance
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
before(async () => {
|
|
9
|
+
const noopHook = { tap: () => {}, untap: () => {}, invoke: async () => {} }
|
|
10
|
+
const noopRouter = { path: '/', createChildRouter: () => noopRouter }
|
|
11
|
+
const mockApp = {
|
|
12
|
+
config: null,
|
|
13
|
+
rootDir: '/test',
|
|
14
|
+
name: 'test-app',
|
|
15
|
+
dependencies: {},
|
|
16
|
+
dependencyloader: { moduleLoadedHook: noopHook },
|
|
17
|
+
waitForModule: async (...names) => {
|
|
18
|
+
const mocks = {
|
|
19
|
+
errors: { LOAD_ERROR: new Error('load') },
|
|
20
|
+
auth: { unsecureRoute: () => {} },
|
|
21
|
+
server: { api: { createChildRouter: () => noopRouter } },
|
|
22
|
+
jsonschema: { createSchema: async () => ({ build: async () => ({}) }) }
|
|
23
|
+
}
|
|
24
|
+
const results = names.map(n => mocks[n] || {})
|
|
25
|
+
return results.length === 1 ? results[0] : results
|
|
26
|
+
},
|
|
27
|
+
errors: { LOAD_ERROR: new Error('load') }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
instance = new ConfigModule(mockApp, {})
|
|
31
|
+
|
|
32
|
+
// Wait for the async init to settle (it will error in test mode, which is fine)
|
|
33
|
+
try { await instance.onReady() } catch (e) { /* expected */ }
|
|
34
|
+
|
|
35
|
+
// Ensure internal state is initialized for testing
|
|
36
|
+
if (!instance._config) instance._config = {}
|
|
37
|
+
if (!instance.publicAttributes) instance.publicAttributes = []
|
|
16
38
|
})
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
39
|
+
|
|
40
|
+
describe('#envVarToConfigKey()', () => {
|
|
41
|
+
it('should convert ADAPT_AUTHORING prefixed env vars to config keys', () => {
|
|
42
|
+
const result = instance.envVarToConfigKey('ADAPT_AUTHORING_SERVER__PORT')
|
|
43
|
+
assert.equal(result, 'adapt-authoring-server.PORT')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should convert underscores to hyphens in module prefix', () => {
|
|
47
|
+
const result = instance.envVarToConfigKey('ADAPT_AUTHORING_MY_MODULE__KEY')
|
|
48
|
+
assert.equal(result, 'adapt-authoring-my-module.KEY')
|
|
21
49
|
})
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
50
|
+
|
|
51
|
+
it('should prefix non-ADAPT_AUTHORING env vars with "env."', () => {
|
|
52
|
+
const result = instance.envVarToConfigKey('NODE_ENV')
|
|
53
|
+
assert.equal(result, 'env.NODE_ENV')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should handle env vars without double underscore', () => {
|
|
57
|
+
// When no __ separator exists, key will be undefined
|
|
58
|
+
// This documents the current behavior of the function
|
|
59
|
+
const result = instance.envVarToConfigKey('ADAPT_AUTHORING_TEST')
|
|
60
|
+
assert.equal(result, 'adapt-authoring-test.undefined')
|
|
25
61
|
})
|
|
26
62
|
})
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
63
|
+
|
|
64
|
+
describe('#has()', () => {
|
|
65
|
+
it('should return true for existing config values', () => {
|
|
66
|
+
instance._config['test.key'] = 'value'
|
|
67
|
+
const exists = instance.has('test.key')
|
|
68
|
+
assert.equal(exists, true)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should return false for non-existent config values', () => {
|
|
72
|
+
const exists = instance.has('nonexistent.key')
|
|
73
|
+
assert.equal(exists, false)
|
|
32
74
|
})
|
|
33
75
|
})
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
76
|
+
|
|
77
|
+
describe('#get()', () => {
|
|
78
|
+
it('should retrieve a stored value', () => {
|
|
79
|
+
instance._config['test.value'] = 'expected'
|
|
80
|
+
const actual = instance.get('test.value')
|
|
81
|
+
assert.equal(actual, 'expected')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('should return undefined for non-existent keys', () => {
|
|
85
|
+
const actual = instance.get('does.not.exist')
|
|
86
|
+
assert.equal(actual, undefined)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should retrieve different data types', () => {
|
|
90
|
+
instance._config['test.string'] = 'text'
|
|
91
|
+
instance._config['test.number'] = 42
|
|
92
|
+
instance._config['test.boolean'] = true
|
|
93
|
+
instance._config['test.object'] = { key: 'value' }
|
|
94
|
+
|
|
95
|
+
assert.equal(instance.get('test.string'), 'text')
|
|
96
|
+
assert.equal(instance.get('test.number'), 42)
|
|
97
|
+
assert.equal(instance.get('test.boolean'), true)
|
|
98
|
+
assert.deepEqual(instance.get('test.object'), { key: 'value' })
|
|
40
99
|
})
|
|
41
100
|
})
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
101
|
+
|
|
102
|
+
describe('#getPublicConfig()', () => {
|
|
103
|
+
it('should return only public attributes', () => {
|
|
104
|
+
instance._config = {
|
|
105
|
+
'module.public1': 'value1',
|
|
106
|
+
'module.public2': 'value2',
|
|
107
|
+
'module.private': 'secret'
|
|
108
|
+
}
|
|
109
|
+
instance.publicAttributes = ['module.public1', 'module.public2']
|
|
110
|
+
|
|
111
|
+
const config = instance.getPublicConfig()
|
|
112
|
+
assert.deepEqual(config, {
|
|
113
|
+
'module.public1': 'value1',
|
|
114
|
+
'module.public2': 'value2'
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('should return empty object when no public attributes exist', () => {
|
|
119
|
+
instance._config = { 'module.private': 'secret' }
|
|
120
|
+
instance.publicAttributes = []
|
|
121
|
+
|
|
122
|
+
const config = instance.getPublicConfig()
|
|
123
|
+
assert.deepEqual(config, {})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should handle undefined values for public attributes', () => {
|
|
127
|
+
instance._config = {}
|
|
128
|
+
instance.publicAttributes = ['module.missing']
|
|
129
|
+
|
|
130
|
+
const config = instance.getPublicConfig()
|
|
131
|
+
assert.deepEqual(config, {
|
|
132
|
+
'module.missing': undefined
|
|
133
|
+
})
|
|
50
134
|
})
|
|
51
135
|
})
|
|
52
136
|
})
|
|
53
|
-
/**
|
|
54
|
-
* Checks ConfigUtility#initialise
|
|
55
|
-
* Loads the testing data in tests/data/dirname
|
|
56
|
-
*/
|
|
57
|
-
function runConfigInitialise (dirname) {
|
|
58
|
-
return function () {
|
|
59
|
-
this.config.app.dependencies = [{ name: 'adapt-authoring-testing', dir: path.join(__dirname, 'data', dirname) }]
|
|
60
|
-
this.config.initialise()
|
|
61
|
-
this.config.errors.length.should.be.exactly(1)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/* global describe, it */
|
|
2
|
-
|
|
3
|
-
const path = require('path')
|
|
4
|
-
const utils = require('../lib/ConfigUtils')
|
|
5
|
-
const should = require('should')
|
|
6
|
-
|
|
7
|
-
describe('Config utils', function () {
|
|
8
|
-
describe('#loadFile()', function () {
|
|
9
|
-
it('should be able to load a valid file', function () {
|
|
10
|
-
const filepath = path.join(__dirname, 'data', 'testfile.json')
|
|
11
|
-
const actualContents = utils.loadFile(filepath)
|
|
12
|
-
const expectedContents = require(filepath)
|
|
13
|
-
actualContents.should.deepEqual(expectedContents)
|
|
14
|
-
})
|
|
15
|
-
it('should not error on a missing file', function () {
|
|
16
|
-
should.doesNotThrow(function () {
|
|
17
|
-
const contents = utils.loadFile(path.join('this', 'path', 'does', 'not', 'exist.xyz'))
|
|
18
|
-
should(contents).be.undefined()
|
|
19
|
-
})
|
|
20
|
-
})
|
|
21
|
-
})
|
|
22
|
-
describe('#loadConfigSchema()', function () {
|
|
23
|
-
it('should be able to load a valid schema file', function () {
|
|
24
|
-
const dir = path.join(__dirname, 'data')
|
|
25
|
-
const actualContents = utils.loadConfigSchema(dir)
|
|
26
|
-
const expectedContents = require(path.join(dir, 'conf', 'config.schema.js'))
|
|
27
|
-
actualContents.should.deepEqual(expectedContents)
|
|
28
|
-
})
|
|
29
|
-
it('should not error on a missing schema file', function () {
|
|
30
|
-
should.doesNotThrow(function () {
|
|
31
|
-
const contents = utils.loadConfigSchema(path.join(__dirname, 'doesntexist'))
|
|
32
|
-
should(contents).be.undefined()
|
|
33
|
-
})
|
|
34
|
-
})
|
|
35
|
-
})
|
|
36
|
-
})
|