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.
@@ -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.1.3",
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
- "adapt-authoring-core": "^1.7.0"
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
+ }