adapt-authoring-errors 1.1.3 → 1.2.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/package.json +7 -9
- package/tests/AdaptError.spec.js +222 -0
- package/tests/ErrorsModule.spec.js +424 -0
- package/tests/data/test-errors.json +21 -0
|
@@ -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@v4
|
|
10
|
+
- uses: actions/setup-node@v4
|
|
11
|
+
with:
|
|
12
|
+
node-version: 'lts/*'
|
|
13
|
+
cache: 'npm'
|
|
14
|
+
- run: npm ci
|
|
15
|
+
- run: npm test
|
package/package.json
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adapt-authoring-errors",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Error handling for the Adapt authoring tool",
|
|
5
5
|
"homepage": "https://github.com/adapt-security/adapt-authoring-errors",
|
|
6
6
|
"license": "GPL-3.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "index.js",
|
|
9
9
|
"repository": "github:adapt-security/adapt-authoring-errors",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test 'tests/**/*.spec.js'"
|
|
12
|
+
},
|
|
10
13
|
"dependencies": {
|
|
14
|
+
"adapt-authoring-core": "^1.7.0",
|
|
11
15
|
"glob": "^13.0.0"
|
|
12
16
|
},
|
|
13
|
-
"peerDependencies": {
|
|
14
|
-
|
|
15
|
-
},
|
|
16
|
-
"peerDependenciesMeta": {
|
|
17
|
-
"adapt-authoring-core": {
|
|
18
|
-
"optional": true
|
|
19
|
-
}
|
|
20
|
-
},
|
|
17
|
+
"peerDependencies": {},
|
|
18
|
+
"peerDependenciesMeta": {},
|
|
21
19
|
"devDependencies": {
|
|
22
20
|
"@semantic-release/git": "^10.0.1",
|
|
23
21
|
"conventional-changelog-eslint": "^6.0.0",
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import AdaptError from '../lib/AdaptError.js'
|
|
4
|
+
|
|
5
|
+
describe('AdaptError', () => {
|
|
6
|
+
describe('constructor', () => {
|
|
7
|
+
it('should create an error with code only', () => {
|
|
8
|
+
const error = new AdaptError('TEST_ERROR')
|
|
9
|
+
assert.equal(error.code, 'TEST_ERROR')
|
|
10
|
+
assert.equal(error.statusCode, 500)
|
|
11
|
+
assert.deepEqual(error.meta, {})
|
|
12
|
+
assert.ok(error instanceof Error)
|
|
13
|
+
assert.ok(error instanceof AdaptError)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should create an error with code and statusCode', () => {
|
|
17
|
+
const error = new AdaptError('NOT_FOUND', 404)
|
|
18
|
+
assert.equal(error.code, 'NOT_FOUND')
|
|
19
|
+
assert.equal(error.statusCode, 404)
|
|
20
|
+
assert.deepEqual(error.meta, {})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should create an error with code, statusCode, and metadata', () => {
|
|
24
|
+
const metadata = { description: 'Test error', data: { id: '123' } }
|
|
25
|
+
const error = new AdaptError('CUSTOM_ERROR', 400, metadata)
|
|
26
|
+
assert.equal(error.code, 'CUSTOM_ERROR')
|
|
27
|
+
assert.equal(error.statusCode, 400)
|
|
28
|
+
assert.deepEqual(error.meta, metadata)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should use default statusCode of 500 when not provided', () => {
|
|
32
|
+
const error = new AdaptError('SERVER_ERROR')
|
|
33
|
+
assert.equal(error.statusCode, 500)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should use empty object as default metadata when not provided', () => {
|
|
37
|
+
const error = new AdaptError('TEST_ERROR', 400)
|
|
38
|
+
assert.deepEqual(error.meta, {})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should set the message property to the error code', () => {
|
|
42
|
+
const error = new AdaptError('MY_ERROR')
|
|
43
|
+
assert.equal(error.message, 'MY_ERROR')
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('#setData()', () => {
|
|
48
|
+
it('should set data on the error', () => {
|
|
49
|
+
const error = new AdaptError('TEST_ERROR')
|
|
50
|
+
const data = { userId: '123', action: 'delete' }
|
|
51
|
+
error.setData(data)
|
|
52
|
+
assert.deepEqual(error.data, data)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should return the error instance for chaining', () => {
|
|
56
|
+
const error = new AdaptError('TEST_ERROR')
|
|
57
|
+
const returnValue = error.setData({ test: 'value' })
|
|
58
|
+
assert.equal(returnValue, error)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should allow method chaining', () => {
|
|
62
|
+
const error = new AdaptError('TEST_ERROR')
|
|
63
|
+
const data = { key: 'value' }
|
|
64
|
+
const result = error.setData(data)
|
|
65
|
+
assert.equal(result.data, data)
|
|
66
|
+
assert.ok(result instanceof AdaptError)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should overwrite existing data', () => {
|
|
70
|
+
const error = new AdaptError('TEST_ERROR')
|
|
71
|
+
error.setData({ first: 'data' })
|
|
72
|
+
error.setData({ second: 'data' })
|
|
73
|
+
assert.deepEqual(error.data, { second: 'data' })
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('#toString()', () => {
|
|
78
|
+
it('should return formatted string without data', () => {
|
|
79
|
+
const error = new AdaptError('TEST_ERROR')
|
|
80
|
+
const result = error.toString()
|
|
81
|
+
// Note: trailing space after code is part of the current implementation
|
|
82
|
+
assert.equal(result, 'AdaptError: TEST_ERROR ')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should return formatted string with data', () => {
|
|
86
|
+
const error = new AdaptError('TEST_ERROR')
|
|
87
|
+
error.setData({ userId: '123' })
|
|
88
|
+
const result = error.toString()
|
|
89
|
+
assert.equal(result, 'AdaptError: TEST_ERROR {"userId":"123"}')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should include class name in output', () => {
|
|
93
|
+
const error = new AdaptError('MY_ERROR')
|
|
94
|
+
const result = error.toString()
|
|
95
|
+
assert.ok(result.startsWith('AdaptError:'))
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should handle complex data objects', () => {
|
|
99
|
+
const error = new AdaptError('COMPLEX_ERROR')
|
|
100
|
+
const complexData = { nested: { key: 'value' }, array: [1, 2, 3] }
|
|
101
|
+
error.setData(complexData)
|
|
102
|
+
const result = error.toString()
|
|
103
|
+
assert.ok(result.includes(JSON.stringify(complexData)))
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('Error properties', () => {
|
|
108
|
+
it('should have code property', () => {
|
|
109
|
+
const error = new AdaptError('TEST_CODE')
|
|
110
|
+
assert.equal(typeof error.code, 'string')
|
|
111
|
+
assert.equal(error.code, 'TEST_CODE')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('should have statusCode property', () => {
|
|
115
|
+
const error = new AdaptError('TEST_ERROR', 404)
|
|
116
|
+
assert.equal(typeof error.statusCode, 'number')
|
|
117
|
+
assert.equal(error.statusCode, 404)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should have meta property', () => {
|
|
121
|
+
const meta = { description: 'Test' }
|
|
122
|
+
const error = new AdaptError('TEST_ERROR', 500, meta)
|
|
123
|
+
assert.equal(typeof error.meta, 'object')
|
|
124
|
+
assert.deepEqual(error.meta, meta)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should have data property after setData is called', () => {
|
|
128
|
+
const error = new AdaptError('TEST_ERROR')
|
|
129
|
+
error.setData({ test: 'data' })
|
|
130
|
+
assert.equal(typeof error.data, 'object')
|
|
131
|
+
assert.deepEqual(error.data, { test: 'data' })
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
describe('Error inheritance', () => {
|
|
136
|
+
it('should have a stack trace', () => {
|
|
137
|
+
const error = new AdaptError('TEST_ERROR')
|
|
138
|
+
assert.equal(typeof error.stack, 'string')
|
|
139
|
+
assert.ok(error.stack.length > 0)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should be catchable as a generic Error', () => {
|
|
143
|
+
let caught = false
|
|
144
|
+
try {
|
|
145
|
+
throw new AdaptError('THROWN_ERROR', 400)
|
|
146
|
+
} catch (e) {
|
|
147
|
+
caught = true
|
|
148
|
+
assert.ok(e instanceof Error)
|
|
149
|
+
assert.equal(e.code, 'THROWN_ERROR')
|
|
150
|
+
assert.equal(e.statusCode, 400)
|
|
151
|
+
}
|
|
152
|
+
assert.ok(caught)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('should inherit name from Error', () => {
|
|
156
|
+
const error = new AdaptError('TEST_ERROR')
|
|
157
|
+
assert.equal(error.name, 'Error')
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe('setData and throw pattern', () => {
|
|
162
|
+
it('should support throw with chained setData', () => {
|
|
163
|
+
const data = { userId: '456' }
|
|
164
|
+
let caught
|
|
165
|
+
try {
|
|
166
|
+
throw new AdaptError('AUTH_ERROR', 401).setData(data)
|
|
167
|
+
} catch (e) {
|
|
168
|
+
caught = e
|
|
169
|
+
}
|
|
170
|
+
assert.ok(caught instanceof AdaptError)
|
|
171
|
+
assert.deepEqual(caught.data, data)
|
|
172
|
+
assert.equal(caught.code, 'AUTH_ERROR')
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
describe('Edge cases', () => {
|
|
177
|
+
it('should handle empty string as error code', () => {
|
|
178
|
+
const error = new AdaptError('')
|
|
179
|
+
assert.equal(error.code, '')
|
|
180
|
+
assert.equal(error.message, '')
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('should handle null metadata', () => {
|
|
184
|
+
const error = new AdaptError('TEST_ERROR', 500, null)
|
|
185
|
+
assert.equal(error.meta, null)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('should handle zero statusCode', () => {
|
|
189
|
+
const error = new AdaptError('TEST_ERROR', 0)
|
|
190
|
+
assert.equal(error.statusCode, 0)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('should handle setData with null', () => {
|
|
194
|
+
const error = new AdaptError('TEST_ERROR')
|
|
195
|
+
error.setData(null)
|
|
196
|
+
assert.equal(error.data, null)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('should handle setData with undefined', () => {
|
|
200
|
+
const error = new AdaptError('TEST_ERROR')
|
|
201
|
+
error.setData(undefined)
|
|
202
|
+
assert.equal(error.data, undefined)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('should handle setData with a string value', () => {
|
|
206
|
+
const error = new AdaptError('TEST_ERROR')
|
|
207
|
+
error.setData('simple string')
|
|
208
|
+
assert.equal(error.data, 'simple string')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('should handle toString with empty data object', () => {
|
|
212
|
+
const error = new AdaptError('TEST_ERROR')
|
|
213
|
+
error.setData({})
|
|
214
|
+
assert.equal(error.toString(), 'AdaptError: TEST_ERROR {}')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('should not have data property before setData is called', () => {
|
|
218
|
+
const error = new AdaptError('TEST_ERROR')
|
|
219
|
+
assert.equal(error.data, undefined)
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
})
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import AdaptError from '../lib/AdaptError.js'
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'
|
|
5
|
+
import { join, dirname } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
|
|
10
|
+
// Note: ErrorsModule depends on adapt-authoring-core which is not available,
|
|
11
|
+
// so we test the core logic that can be tested independently
|
|
12
|
+
describe('ErrorsModule', () => {
|
|
13
|
+
let tempDir
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// Create a temporary directory for test errors
|
|
17
|
+
tempDir = join(__dirname, 'temp-test-errors')
|
|
18
|
+
mkdirSync(tempDir, { recursive: true })
|
|
19
|
+
mkdirSync(join(tempDir, 'errors'), { recursive: true })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
// Cleanup temp directory
|
|
24
|
+
if (tempDir) {
|
|
25
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('Error definition structure', () => {
|
|
30
|
+
it('should validate error definition format', () => {
|
|
31
|
+
const errorDef = {
|
|
32
|
+
description: 'Test error description',
|
|
33
|
+
statusCode: 404,
|
|
34
|
+
data: {
|
|
35
|
+
id: 'Item identifier'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
assert.ok(errorDef.description)
|
|
40
|
+
assert.equal(typeof errorDef.description, 'string')
|
|
41
|
+
assert.ok(errorDef.statusCode)
|
|
42
|
+
assert.equal(typeof errorDef.statusCode, 'number')
|
|
43
|
+
if (errorDef.data) {
|
|
44
|
+
assert.equal(typeof errorDef.data, 'object')
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should create AdaptError from definition', () => {
|
|
49
|
+
const errorDef = {
|
|
50
|
+
description: 'Test error',
|
|
51
|
+
statusCode: 500,
|
|
52
|
+
data: {
|
|
53
|
+
field: 'Test field'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const metadata = { description: errorDef.description }
|
|
58
|
+
if (errorDef.data) {
|
|
59
|
+
metadata.data = errorDef.data
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const error = new AdaptError('TEST_CODE', errorDef.statusCode, metadata)
|
|
63
|
+
|
|
64
|
+
assert.ok(error instanceof AdaptError)
|
|
65
|
+
assert.equal(error.code, 'TEST_CODE')
|
|
66
|
+
assert.equal(error.statusCode, 500)
|
|
67
|
+
assert.equal(error.meta.description, 'Test error')
|
|
68
|
+
assert.deepEqual(error.meta.data, { field: 'Test field' })
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should handle error definition without data field', () => {
|
|
72
|
+
const errorDef = {
|
|
73
|
+
description: 'Simple error',
|
|
74
|
+
statusCode: 400
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const metadata = { description: errorDef.description }
|
|
78
|
+
const error = new AdaptError('SIMPLE_ERROR', errorDef.statusCode, metadata)
|
|
79
|
+
|
|
80
|
+
assert.ok(error.meta.description)
|
|
81
|
+
assert.equal(error.meta.data, undefined)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('Error JSON file format', () => {
|
|
86
|
+
it('should parse valid error JSON file', () => {
|
|
87
|
+
const errorDefs = {
|
|
88
|
+
ERROR_ONE: {
|
|
89
|
+
description: 'First error',
|
|
90
|
+
statusCode: 400
|
|
91
|
+
},
|
|
92
|
+
ERROR_TWO: {
|
|
93
|
+
description: 'Second error',
|
|
94
|
+
statusCode: 404,
|
|
95
|
+
data: {
|
|
96
|
+
id: 'Identifier'
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
writeFileSync(
|
|
102
|
+
join(tempDir, 'errors', 'test.json'),
|
|
103
|
+
JSON.stringify(errorDefs)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const content = JSON.parse(
|
|
107
|
+
readFileSync(join(tempDir, 'errors', 'test.json'), 'utf8')
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
assert.ok(content.ERROR_ONE)
|
|
111
|
+
assert.ok(content.ERROR_TWO)
|
|
112
|
+
assert.equal(content.ERROR_ONE.statusCode, 400)
|
|
113
|
+
assert.equal(content.ERROR_TWO.statusCode, 404)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should validate error codes are uppercase with underscores', () => {
|
|
117
|
+
const validCodes = ['TEST_ERROR', 'NOT_FOUND', 'SERVER_ERROR', 'MY_CUSTOM_ERROR']
|
|
118
|
+
|
|
119
|
+
validCodes.forEach(code => {
|
|
120
|
+
assert.ok(/^[A-Z_]+$/.test(code), `${code} should be uppercase with underscores`)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should handle multiple error JSON files', () => {
|
|
125
|
+
writeFileSync(
|
|
126
|
+
join(tempDir, 'errors', 'set-a.json'),
|
|
127
|
+
JSON.stringify({ ERR_A: { description: 'Error A', statusCode: 400 } })
|
|
128
|
+
)
|
|
129
|
+
writeFileSync(
|
|
130
|
+
join(tempDir, 'errors', 'set-b.json'),
|
|
131
|
+
JSON.stringify({ ERR_B: { description: 'Error B', statusCode: 404 } })
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
const contentA = JSON.parse(
|
|
135
|
+
readFileSync(join(tempDir, 'errors', 'set-a.json'), 'utf8')
|
|
136
|
+
)
|
|
137
|
+
const contentB = JSON.parse(
|
|
138
|
+
readFileSync(join(tempDir, 'errors', 'set-b.json'), 'utf8')
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
const merged = { ...contentA, ...contentB }
|
|
142
|
+
assert.ok(merged.ERR_A)
|
|
143
|
+
assert.ok(merged.ERR_B)
|
|
144
|
+
assert.equal(Object.keys(merged).length, 2)
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('Error definition examples', () => {
|
|
149
|
+
it('should demonstrate typical error patterns', () => {
|
|
150
|
+
const errorPatterns = [
|
|
151
|
+
{ code: 'NOT_FOUND', statusCode: 404, description: 'Resource not found' },
|
|
152
|
+
{ code: 'UNAUTHORIZED', statusCode: 401, description: 'Authentication required' },
|
|
153
|
+
{ code: 'FORBIDDEN', statusCode: 403, description: 'Access denied' },
|
|
154
|
+
{ code: 'BAD_REQUEST', statusCode: 400, description: 'Invalid request' },
|
|
155
|
+
{ code: 'SERVER_ERROR', statusCode: 500, description: 'Internal server error' }
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
errorPatterns.forEach(pattern => {
|
|
159
|
+
const error = new AdaptError(pattern.code, pattern.statusCode, { description: pattern.description })
|
|
160
|
+
assert.equal(error.code, pattern.code)
|
|
161
|
+
assert.equal(error.statusCode, pattern.statusCode)
|
|
162
|
+
assert.equal(error.meta.description, pattern.description)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('Error sorting', () => {
|
|
168
|
+
it('should sort error codes alphabetically', () => {
|
|
169
|
+
const unsortedCodes = ['ZEBRA_ERROR', 'ALPHA_ERROR', 'MIDDLE_ERROR', 'BETA_ERROR']
|
|
170
|
+
const sortedCodes = [...unsortedCodes].sort()
|
|
171
|
+
|
|
172
|
+
assert.deepEqual(sortedCodes, ['ALPHA_ERROR', 'BETA_ERROR', 'MIDDLE_ERROR', 'ZEBRA_ERROR'])
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
describe('Error metadata handling', () => {
|
|
177
|
+
it('should preserve description in metadata', () => {
|
|
178
|
+
const description = 'A detailed error description'
|
|
179
|
+
const metadata = { description }
|
|
180
|
+
const error = new AdaptError('TEST', 500, metadata)
|
|
181
|
+
|
|
182
|
+
assert.equal(error.meta.description, description)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('should preserve data schema in metadata', () => {
|
|
186
|
+
const data = {
|
|
187
|
+
userId: 'User identifier',
|
|
188
|
+
action: 'The action being performed'
|
|
189
|
+
}
|
|
190
|
+
const metadata = { description: 'Test', data }
|
|
191
|
+
const error = new AdaptError('TEST', 500, metadata)
|
|
192
|
+
|
|
193
|
+
assert.deepEqual(error.meta.data, data)
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
describe('loadErrors reduction logic', () => {
|
|
198
|
+
it('should create getter properties that return AdaptError instances', () => {
|
|
199
|
+
const errorDefs = {
|
|
200
|
+
TEST_ERR: { description: 'A test', statusCode: 500 }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const result = Object.entries(errorDefs)
|
|
204
|
+
.sort()
|
|
205
|
+
.reduce((m, [k, { description, statusCode, data }]) => {
|
|
206
|
+
return Object.defineProperty(m, k, {
|
|
207
|
+
get: () => {
|
|
208
|
+
const metadata = { description }
|
|
209
|
+
if (data) metadata.data = data
|
|
210
|
+
return new AdaptError(k, statusCode, metadata)
|
|
211
|
+
},
|
|
212
|
+
enumerable: true
|
|
213
|
+
})
|
|
214
|
+
}, {})
|
|
215
|
+
|
|
216
|
+
const error = result.TEST_ERR
|
|
217
|
+
assert.ok(error instanceof AdaptError)
|
|
218
|
+
assert.equal(error.code, 'TEST_ERR')
|
|
219
|
+
assert.equal(error.statusCode, 500)
|
|
220
|
+
assert.equal(error.meta.description, 'A test')
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('should return a new instance on each property access', () => {
|
|
224
|
+
const errorDefs = {
|
|
225
|
+
MY_ERROR: { description: 'Repeated', statusCode: 400 }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const result = Object.entries(errorDefs)
|
|
229
|
+
.sort()
|
|
230
|
+
.reduce((m, [k, { description, statusCode, data }]) => {
|
|
231
|
+
return Object.defineProperty(m, k, {
|
|
232
|
+
get: () => {
|
|
233
|
+
const metadata = { description }
|
|
234
|
+
if (data) metadata.data = data
|
|
235
|
+
return new AdaptError(k, statusCode, metadata)
|
|
236
|
+
},
|
|
237
|
+
enumerable: true
|
|
238
|
+
})
|
|
239
|
+
}, {})
|
|
240
|
+
|
|
241
|
+
const first = result.MY_ERROR
|
|
242
|
+
const second = result.MY_ERROR
|
|
243
|
+
assert.notEqual(first, second)
|
|
244
|
+
assert.equal(first.code, second.code)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('should include data in metadata when defined', () => {
|
|
248
|
+
const errorDefs = {
|
|
249
|
+
DATA_ERR: { description: 'Has data', statusCode: 404, data: { id: 'Item ID' } }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const result = Object.entries(errorDefs)
|
|
253
|
+
.sort()
|
|
254
|
+
.reduce((m, [k, { description, statusCode, data }]) => {
|
|
255
|
+
return Object.defineProperty(m, k, {
|
|
256
|
+
get: () => {
|
|
257
|
+
const metadata = { description }
|
|
258
|
+
if (data) metadata.data = data
|
|
259
|
+
return new AdaptError(k, statusCode, metadata)
|
|
260
|
+
},
|
|
261
|
+
enumerable: true
|
|
262
|
+
})
|
|
263
|
+
}, {})
|
|
264
|
+
|
|
265
|
+
const error = result.DATA_ERR
|
|
266
|
+
assert.deepEqual(error.meta.data, { id: 'Item ID' })
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('should omit data from metadata when not defined', () => {
|
|
270
|
+
const errorDefs = {
|
|
271
|
+
NO_DATA_ERR: { description: 'No data', statusCode: 500 }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const result = Object.entries(errorDefs)
|
|
275
|
+
.sort()
|
|
276
|
+
.reduce((m, [k, { description, statusCode, data }]) => {
|
|
277
|
+
return Object.defineProperty(m, k, {
|
|
278
|
+
get: () => {
|
|
279
|
+
const metadata = { description }
|
|
280
|
+
if (data) metadata.data = data
|
|
281
|
+
return new AdaptError(k, statusCode, metadata)
|
|
282
|
+
},
|
|
283
|
+
enumerable: true
|
|
284
|
+
})
|
|
285
|
+
}, {})
|
|
286
|
+
|
|
287
|
+
const error = result.NO_DATA_ERR
|
|
288
|
+
assert.equal(error.meta.data, undefined)
|
|
289
|
+
assert.ok(!('data' in error.meta))
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('should sort error codes alphabetically in reduced result', () => {
|
|
293
|
+
const errorDefs = {
|
|
294
|
+
ZEBRA: { description: 'Z', statusCode: 500 },
|
|
295
|
+
ALPHA: { description: 'A', statusCode: 500 },
|
|
296
|
+
MIDDLE: { description: 'M', statusCode: 500 }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const result = Object.entries(errorDefs)
|
|
300
|
+
.sort()
|
|
301
|
+
.reduce((m, [k, { description, statusCode, data }]) => {
|
|
302
|
+
return Object.defineProperty(m, k, {
|
|
303
|
+
get: () => {
|
|
304
|
+
const metadata = { description }
|
|
305
|
+
if (data) metadata.data = data
|
|
306
|
+
return new AdaptError(k, statusCode, metadata)
|
|
307
|
+
},
|
|
308
|
+
enumerable: true
|
|
309
|
+
})
|
|
310
|
+
}, {})
|
|
311
|
+
|
|
312
|
+
const keys = Object.keys(result)
|
|
313
|
+
assert.deepEqual(keys, ['ALPHA', 'MIDDLE', 'ZEBRA'])
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('should make error properties enumerable', () => {
|
|
317
|
+
const errorDefs = {
|
|
318
|
+
ENUM_ERR: { description: 'Enumerable', statusCode: 500 }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const result = Object.entries(errorDefs)
|
|
322
|
+
.sort()
|
|
323
|
+
.reduce((m, [k, { description, statusCode, data }]) => {
|
|
324
|
+
return Object.defineProperty(m, k, {
|
|
325
|
+
get: () => {
|
|
326
|
+
const metadata = { description }
|
|
327
|
+
if (data) metadata.data = data
|
|
328
|
+
return new AdaptError(k, statusCode, metadata)
|
|
329
|
+
},
|
|
330
|
+
enumerable: true
|
|
331
|
+
})
|
|
332
|
+
}, {})
|
|
333
|
+
|
|
334
|
+
const descriptor = Object.getOwnPropertyDescriptor(result, 'ENUM_ERR')
|
|
335
|
+
assert.equal(descriptor.enumerable, true)
|
|
336
|
+
assert.equal(typeof descriptor.get, 'function')
|
|
337
|
+
})
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
describe('Shipped error definitions', () => {
|
|
341
|
+
it('should have valid node-core error definitions', () => {
|
|
342
|
+
const nodeCoreErrors = JSON.parse(
|
|
343
|
+
readFileSync(join(__dirname, '..', 'errors', 'node-core.json'), 'utf8')
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
const expectedCodes = ['EACCES', 'EADDRINUSE', 'ECONNREFUSED', 'EEXIST', 'ENOENT', 'ENOTEMPTY', 'MODULE_NOT_FOUND']
|
|
347
|
+
expectedCodes.forEach(code => {
|
|
348
|
+
assert.ok(nodeCoreErrors[code], `Missing expected error code: ${code}`)
|
|
349
|
+
assert.equal(typeof nodeCoreErrors[code].description, 'string')
|
|
350
|
+
assert.equal(typeof nodeCoreErrors[code].statusCode, 'number')
|
|
351
|
+
})
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('should have valid adapt error definitions', () => {
|
|
355
|
+
const adaptErrors = JSON.parse(
|
|
356
|
+
readFileSync(join(__dirname, '..', 'errors', 'adapt-errors.json'), 'utf8')
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
const expectedCodes = ['FUNC_NOT_OVERRIDDEN', 'FUNC_DISABLED', 'SERVER_ERROR', 'INVALID_PARAMS', 'NOT_FOUND']
|
|
360
|
+
expectedCodes.forEach(code => {
|
|
361
|
+
assert.ok(adaptErrors[code], `Missing expected error code: ${code}`)
|
|
362
|
+
assert.equal(typeof adaptErrors[code].description, 'string')
|
|
363
|
+
assert.equal(typeof adaptErrors[code].statusCode, 'number')
|
|
364
|
+
})
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it('should have valid test error definitions', () => {
|
|
368
|
+
const testErrors = JSON.parse(
|
|
369
|
+
readFileSync(join(__dirname, 'data', 'test-errors.json'), 'utf8')
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
assert.ok(testErrors.TEST_ERROR)
|
|
373
|
+
assert.ok(testErrors.TEST_NOT_FOUND)
|
|
374
|
+
assert.ok(testErrors.TEST_VALIDATION_ERROR)
|
|
375
|
+
assert.equal(testErrors.TEST_NOT_FOUND.statusCode, 404)
|
|
376
|
+
assert.equal(testErrors.TEST_VALIDATION_ERROR.statusCode, 400)
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('should use uppercase with underscores for all error codes', () => {
|
|
380
|
+
const files = ['node-core.json', 'adapt-errors.json']
|
|
381
|
+
files.forEach(file => {
|
|
382
|
+
const errors = JSON.parse(
|
|
383
|
+
readFileSync(join(__dirname, '..', 'errors', file), 'utf8')
|
|
384
|
+
)
|
|
385
|
+
Object.keys(errors).forEach(code => {
|
|
386
|
+
assert.ok(/^[A-Z][A-Z0-9_]*$/.test(code), `Invalid error code format: ${code} in ${file}`)
|
|
387
|
+
})
|
|
388
|
+
})
|
|
389
|
+
})
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
describe('Duplicate error detection', () => {
|
|
393
|
+
it('should detect duplicate error codes across files', () => {
|
|
394
|
+
writeFileSync(
|
|
395
|
+
join(tempDir, 'errors', 'first.json'),
|
|
396
|
+
JSON.stringify({ DUPLICATE: { description: 'First', statusCode: 500 } })
|
|
397
|
+
)
|
|
398
|
+
writeFileSync(
|
|
399
|
+
join(tempDir, 'errors', 'second.json'),
|
|
400
|
+
JSON.stringify({ DUPLICATE: { description: 'Second', statusCode: 400 } })
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
const allDefs = {}
|
|
404
|
+
const duplicates = []
|
|
405
|
+
|
|
406
|
+
const files = ['first.json', 'second.json']
|
|
407
|
+
files.forEach(file => {
|
|
408
|
+
const contents = JSON.parse(
|
|
409
|
+
readFileSync(join(tempDir, 'errors', file), 'utf8')
|
|
410
|
+
)
|
|
411
|
+
Object.entries(contents).forEach(([k, v]) => {
|
|
412
|
+
if (allDefs[k]) {
|
|
413
|
+
duplicates.push(k)
|
|
414
|
+
} else {
|
|
415
|
+
allDefs[k] = v
|
|
416
|
+
}
|
|
417
|
+
})
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
assert.equal(duplicates.length, 1)
|
|
421
|
+
assert.equal(duplicates[0], 'DUPLICATE')
|
|
422
|
+
})
|
|
423
|
+
})
|
|
424
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"TEST_ERROR": {
|
|
3
|
+
"description": "A test error",
|
|
4
|
+
"statusCode": 500
|
|
5
|
+
},
|
|
6
|
+
"TEST_NOT_FOUND": {
|
|
7
|
+
"description": "Test item not found",
|
|
8
|
+
"statusCode": 404,
|
|
9
|
+
"data": {
|
|
10
|
+
"id": "Item identifier"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"TEST_VALIDATION_ERROR": {
|
|
14
|
+
"description": "Test validation failed",
|
|
15
|
+
"statusCode": 400,
|
|
16
|
+
"data": {
|
|
17
|
+
"field": "The invalid field",
|
|
18
|
+
"value": "The invalid value"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|