adapt-authoring-server 1.3.0 → 1.4.1
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 +4 -1
- package/tests/Router.spec.js +552 -0
- package/tests/ServerModule.spec.js +163 -0
- package/tests/ServerUtils.spec.js +555 -0
- package/tests/data/AbstractModuleMock.js +27 -0
- package/tests/data/AppMock.js +17 -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@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/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adapt-authoring-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Provides an Express application routing and more",
|
|
5
5
|
"homepage": "https://github.com/adapt-security/adapt-authoring-server",
|
|
6
6
|
"license": "GPL-3.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "index.js",
|
|
9
9
|
"repository": "github:adapt-security/adapt-authoring-server",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test tests/**/*.spec.js"
|
|
12
|
+
},
|
|
10
13
|
"dependencies": {
|
|
11
14
|
"adapt-authoring-core": "^1.7.0",
|
|
12
15
|
"express": "^5.1.0",
|
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import { describe, it, before } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import express from 'express'
|
|
4
|
+
import { App } from 'adapt-authoring-core'
|
|
5
|
+
|
|
6
|
+
describe('Router', () => {
|
|
7
|
+
let Router
|
|
8
|
+
let mockApp
|
|
9
|
+
|
|
10
|
+
before(async () => {
|
|
11
|
+
const app = App.instance
|
|
12
|
+
app.logger = { log: () => {}, name: 'adapt-authoring-logger' }
|
|
13
|
+
app.dependencyloader.instances = app.dependencyloader.instances || {}
|
|
14
|
+
app.dependencyloader.instances['adapt-authoring-server'] = { url: 'http://localhost:5000' }
|
|
15
|
+
// Wait for App singleton's async init/setReady to settle and
|
|
16
|
+
// reset exitCode set by App.init() failure (expected in test context)
|
|
17
|
+
await app.onReady().catch(() => {})
|
|
18
|
+
process.exitCode = 0
|
|
19
|
+
|
|
20
|
+
const routerModule = await import('../lib/Router.js')
|
|
21
|
+
Router = routerModule.default
|
|
22
|
+
mockApp = express()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('constructor', () => {
|
|
26
|
+
it('should create a router with default values', () => {
|
|
27
|
+
const router = new Router('/test', mockApp)
|
|
28
|
+
|
|
29
|
+
assert.equal(router.root, '/test')
|
|
30
|
+
assert.equal(typeof router.expressRouter, 'function')
|
|
31
|
+
assert.ok(Array.isArray(router.routes))
|
|
32
|
+
assert.equal(router.routes.length, 0)
|
|
33
|
+
assert.ok(Array.isArray(router.childRouters))
|
|
34
|
+
assert.equal(router.childRouters.length, 0)
|
|
35
|
+
assert.ok(Array.isArray(router.routerMiddleware))
|
|
36
|
+
assert.ok(Array.isArray(router.handlerMiddleware))
|
|
37
|
+
assert.equal(router._initialised, false)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should store the parent router', () => {
|
|
41
|
+
const router = new Router('/test', mockApp)
|
|
42
|
+
|
|
43
|
+
assert.equal(router.parentRouter, mockApp)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should filter out non-function middleware', () => {
|
|
47
|
+
const fn1 = () => {}
|
|
48
|
+
const fn2 = () => {}
|
|
49
|
+
const middleware = [fn1, 'not a function', fn2, 42, null]
|
|
50
|
+
const router = new Router('/test', mockApp, [], middleware)
|
|
51
|
+
|
|
52
|
+
assert.ok(router.routerMiddleware.includes(fn1))
|
|
53
|
+
assert.ok(router.routerMiddleware.includes(fn2))
|
|
54
|
+
assert.ok(!router.routerMiddleware.includes('not a function'))
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should filter out non-function handler middleware', () => {
|
|
58
|
+
const fn = () => {}
|
|
59
|
+
const handlerMiddleware = [fn, 'bad', undefined]
|
|
60
|
+
const router = new Router('/test', mockApp, [], [], handlerMiddleware)
|
|
61
|
+
|
|
62
|
+
assert.ok(router.handlerMiddleware.includes(fn))
|
|
63
|
+
assert.ok(!router.handlerMiddleware.includes('bad'))
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should filter invalid routes passed to constructor', () => {
|
|
67
|
+
const validRoute = { route: '/users', handlers: { get: () => {} } }
|
|
68
|
+
const invalidRoute = { route: 123, handlers: { get: () => {} } }
|
|
69
|
+
const router = new Router('/test', mockApp, [validRoute, invalidRoute])
|
|
70
|
+
|
|
71
|
+
assert.equal(router.routes.length, 1)
|
|
72
|
+
assert.equal(router.routes[0].route, '/users')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should include default router middleware (addErrorHandler)', () => {
|
|
76
|
+
const router = new Router('/test', mockApp)
|
|
77
|
+
|
|
78
|
+
assert.ok(router.routerMiddleware.length >= 1)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should include default handler middleware (addExistenceProps, handleInternalRoutes)', () => {
|
|
82
|
+
const router = new Router('/test', mockApp)
|
|
83
|
+
|
|
84
|
+
assert.ok(router.handlerMiddleware.length >= 2)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('#path', () => {
|
|
89
|
+
it('should generate path from parent router', () => {
|
|
90
|
+
const parentRouter = new Router('/api', mockApp)
|
|
91
|
+
const childRouter = new Router('users', parentRouter)
|
|
92
|
+
|
|
93
|
+
assert.equal(childRouter.path, '/api/users')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('should handle trailing slashes correctly', () => {
|
|
97
|
+
const parentRouter = new Router('/api/', mockApp)
|
|
98
|
+
const childRouter = new Router('/users', parentRouter)
|
|
99
|
+
|
|
100
|
+
// Documents existing behavior: double slashes are allowed
|
|
101
|
+
assert.equal(childRouter.path, '/api//users')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should handle root path without parent path property', () => {
|
|
105
|
+
const router = new Router('/test', mockApp)
|
|
106
|
+
|
|
107
|
+
assert.equal(router.path, '/test')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should build multi-level nested paths', () => {
|
|
111
|
+
const root = new Router('/api', mockApp)
|
|
112
|
+
const child = new Router('v1', root)
|
|
113
|
+
const grandchild = new Router('users', child)
|
|
114
|
+
|
|
115
|
+
assert.equal(grandchild.path, '/api/v1/users')
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe('#url', () => {
|
|
120
|
+
it('should return server url + path', () => {
|
|
121
|
+
const router = new Router('/api', mockApp)
|
|
122
|
+
|
|
123
|
+
assert.equal(router.url, 'http://localhost:5000/api')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should include full hierarchy path', () => {
|
|
127
|
+
const parent = new Router('/api', mockApp)
|
|
128
|
+
const child = new Router('users', parent)
|
|
129
|
+
|
|
130
|
+
assert.equal(child.url, 'http://localhost:5000/api/users')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should return empty string when server instance is not available', () => {
|
|
134
|
+
const origInstances = App.instance.dependencyloader.instances
|
|
135
|
+
App.instance.dependencyloader.instances = {}
|
|
136
|
+
const router = new Router('/api', mockApp)
|
|
137
|
+
|
|
138
|
+
assert.equal(router.url, '')
|
|
139
|
+
|
|
140
|
+
App.instance.dependencyloader.instances = origInstances
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('#map', () => {
|
|
145
|
+
it('should return a router map object', () => {
|
|
146
|
+
const api = new Router('api', mockApp)
|
|
147
|
+
api.createChildRouter('test', [
|
|
148
|
+
{ route: '/test', handlers: { get: () => {} }, post: () => {} },
|
|
149
|
+
{ route: '/test2', handlers: { patch: () => {} } }
|
|
150
|
+
])
|
|
151
|
+
|
|
152
|
+
const map = api.map
|
|
153
|
+
|
|
154
|
+
assert.equal(typeof map, 'object')
|
|
155
|
+
assert.equal(map.test_endpoints.length, 2)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('should return empty object for router with no routes', () => {
|
|
159
|
+
const router = new Router('api', mockApp)
|
|
160
|
+
|
|
161
|
+
assert.deepEqual(router.map, {})
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe('#addMiddleware()', () => {
|
|
166
|
+
it('should add middleware to router stack', () => {
|
|
167
|
+
const router = new Router('/test', mockApp)
|
|
168
|
+
const middleware = () => {}
|
|
169
|
+
const initialLength = router.routerMiddleware.length
|
|
170
|
+
|
|
171
|
+
router.addMiddleware(middleware)
|
|
172
|
+
|
|
173
|
+
assert.equal(router.routerMiddleware.length, initialLength + 1)
|
|
174
|
+
assert.ok(router.routerMiddleware.includes(middleware))
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('should return router instance for chaining', () => {
|
|
178
|
+
const router = new Router('/test', mockApp)
|
|
179
|
+
const result = router.addMiddleware(() => {})
|
|
180
|
+
|
|
181
|
+
assert.equal(result, router)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should not add duplicate middleware', () => {
|
|
185
|
+
const router = new Router('/test', mockApp)
|
|
186
|
+
const middleware = () => {}
|
|
187
|
+
|
|
188
|
+
router.addMiddleware(middleware)
|
|
189
|
+
const lengthAfterFirst = router.routerMiddleware.length
|
|
190
|
+
router.addMiddleware(middleware)
|
|
191
|
+
|
|
192
|
+
assert.equal(router.routerMiddleware.length, lengthAfterFirst)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('should add multiple middleware in one call', () => {
|
|
196
|
+
const router = new Router('/test', mockApp)
|
|
197
|
+
const fn1 = () => {}
|
|
198
|
+
const fn2 = () => {}
|
|
199
|
+
const initialLength = router.routerMiddleware.length
|
|
200
|
+
|
|
201
|
+
router.addMiddleware(fn1, fn2)
|
|
202
|
+
|
|
203
|
+
assert.equal(router.routerMiddleware.length, initialLength + 2)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('should not add non-function values', () => {
|
|
207
|
+
const router = new Router('/test', mockApp)
|
|
208
|
+
const initialLength = router.routerMiddleware.length
|
|
209
|
+
|
|
210
|
+
router.addMiddleware('not a function')
|
|
211
|
+
|
|
212
|
+
assert.equal(router.routerMiddleware.length, initialLength)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('should return router when called with no arguments', () => {
|
|
216
|
+
const router = new Router('/test', mockApp)
|
|
217
|
+
const result = router.addMiddleware()
|
|
218
|
+
|
|
219
|
+
assert.equal(result, router)
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe('#addHandlerMiddleware()', () => {
|
|
224
|
+
it('should add handler middleware to stack', () => {
|
|
225
|
+
const router = new Router('/test', mockApp)
|
|
226
|
+
const middleware = () => {}
|
|
227
|
+
const initialLength = router.handlerMiddleware.length
|
|
228
|
+
|
|
229
|
+
router.addHandlerMiddleware(middleware)
|
|
230
|
+
|
|
231
|
+
assert.equal(router.handlerMiddleware.length, initialLength + 1)
|
|
232
|
+
assert.ok(router.handlerMiddleware.includes(middleware))
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('should return router instance for chaining', () => {
|
|
236
|
+
const router = new Router('/test', mockApp)
|
|
237
|
+
const result = router.addHandlerMiddleware(() => {})
|
|
238
|
+
|
|
239
|
+
assert.equal(result, router)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('should not add duplicate handler middleware', () => {
|
|
243
|
+
const router = new Router('/test', mockApp)
|
|
244
|
+
const middleware = () => {}
|
|
245
|
+
|
|
246
|
+
router.addHandlerMiddleware(middleware)
|
|
247
|
+
const lengthAfterFirst = router.handlerMiddleware.length
|
|
248
|
+
router.addHandlerMiddleware(middleware)
|
|
249
|
+
|
|
250
|
+
assert.equal(router.handlerMiddleware.length, lengthAfterFirst)
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
describe('#getHandlerMiddleware()', () => {
|
|
255
|
+
it('should get middleware from router hierarchy', () => {
|
|
256
|
+
const parentRouter = new Router('/api', mockApp)
|
|
257
|
+
const childRouter = new Router('users', parentRouter)
|
|
258
|
+
const middleware = () => {}
|
|
259
|
+
|
|
260
|
+
parentRouter.addHandlerMiddleware(middleware)
|
|
261
|
+
const result = childRouter.getHandlerMiddleware()
|
|
262
|
+
|
|
263
|
+
assert.ok(Array.isArray(result))
|
|
264
|
+
assert.ok(result.includes(middleware))
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('should stop recursing when parent is not a Router', () => {
|
|
268
|
+
const router = new Router('/test', mockApp)
|
|
269
|
+
const result = router.getHandlerMiddleware()
|
|
270
|
+
|
|
271
|
+
assert.ok(Array.isArray(result))
|
|
272
|
+
assert.ok(result.length > 0)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('should return unique middleware (deduplicated)', () => {
|
|
276
|
+
const parentRouter = new Router('/api', mockApp)
|
|
277
|
+
const childRouter = new Router('users', parentRouter)
|
|
278
|
+
|
|
279
|
+
const result = childRouter.getHandlerMiddleware()
|
|
280
|
+
const unique = [...new Set(result)]
|
|
281
|
+
|
|
282
|
+
assert.equal(result.length, unique.length)
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
describe('#addRoute()', () => {
|
|
287
|
+
it('should add valid route', () => {
|
|
288
|
+
const router = new Router('/test', mockApp)
|
|
289
|
+
const route = { route: '/users', handlers: { get: () => {} } }
|
|
290
|
+
|
|
291
|
+
router.addRoute(route)
|
|
292
|
+
|
|
293
|
+
assert.equal(router.routes.length, 1)
|
|
294
|
+
assert.equal(router.routes[0].route, '/users')
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('should return router instance for chaining', () => {
|
|
298
|
+
const router = new Router('/test', mockApp)
|
|
299
|
+
const route = { route: '/users', handlers: { get: () => {} } }
|
|
300
|
+
const result = router.addRoute(route)
|
|
301
|
+
|
|
302
|
+
assert.equal(result, router)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('should add multiple routes in one call', () => {
|
|
306
|
+
const router = new Router('/test', mockApp)
|
|
307
|
+
const route1 = { route: '/users', handlers: { get: () => {} } }
|
|
308
|
+
const route2 = { route: '/posts', handlers: { post: () => {} } }
|
|
309
|
+
|
|
310
|
+
router.addRoute(route1, route2)
|
|
311
|
+
|
|
312
|
+
assert.equal(router.routes.length, 2)
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('should filter out invalid routes', () => {
|
|
316
|
+
const router = new Router('/test', mockApp)
|
|
317
|
+
const valid = { route: '/users', handlers: { get: () => {} } }
|
|
318
|
+
const invalid = { route: 123, handlers: {} }
|
|
319
|
+
|
|
320
|
+
router.addRoute(valid, invalid)
|
|
321
|
+
|
|
322
|
+
assert.equal(router.routes.length, 1)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('should not add routes after initialisation', () => {
|
|
326
|
+
const router = new Router('/test', mockApp)
|
|
327
|
+
router.init()
|
|
328
|
+
|
|
329
|
+
router.addRoute({ route: '/late', handlers: { get: () => {} } })
|
|
330
|
+
|
|
331
|
+
assert.equal(router.routes.length, 0)
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
describe('#validateRoute()', () => {
|
|
336
|
+
it('should return true for valid route', () => {
|
|
337
|
+
const router = new Router('/test', mockApp)
|
|
338
|
+
const route = { route: '/users', handlers: { get: () => {} } }
|
|
339
|
+
|
|
340
|
+
assert.equal(router.validateRoute(route), true)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('should return false if route is not a string', () => {
|
|
344
|
+
const router = new Router('/test', mockApp)
|
|
345
|
+
|
|
346
|
+
assert.equal(router.validateRoute({ route: 123, handlers: { get: () => {} } }), false)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('should return false if handlers is missing', () => {
|
|
350
|
+
const router = new Router('/test', mockApp)
|
|
351
|
+
|
|
352
|
+
assert.equal(router.validateRoute({ route: '/test' }), false)
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('should return false if handler method is not a valid Express method', () => {
|
|
356
|
+
const router = new Router('/test', mockApp)
|
|
357
|
+
|
|
358
|
+
assert.equal(router.validateRoute({ route: '/test', handlers: { badmethod: () => {} } }), false)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it('should return false if handler value is not a function or array of functions', () => {
|
|
362
|
+
const router = new Router('/test', mockApp)
|
|
363
|
+
|
|
364
|
+
assert.equal(router.validateRoute({ route: '/test', handlers: { get: 'not a function' } }), false)
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it('should accept array of functions as handler', () => {
|
|
368
|
+
const router = new Router('/test', mockApp)
|
|
369
|
+
const route = { route: '/users', handlers: { get: [() => {}, () => {}] } }
|
|
370
|
+
|
|
371
|
+
assert.equal(router.validateRoute(route), true)
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
it('should return false if handler array contains non-functions', () => {
|
|
375
|
+
const router = new Router('/test', mockApp)
|
|
376
|
+
const route = { route: '/users', handlers: { get: [() => {}, 'not a function'] } }
|
|
377
|
+
|
|
378
|
+
assert.equal(router.validateRoute(route), false)
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it('should accept multiple HTTP methods', () => {
|
|
382
|
+
const router = new Router('/test', mockApp)
|
|
383
|
+
const route = { route: '/users', handlers: { get: () => {}, post: () => {}, put: () => {}, delete: () => {} } }
|
|
384
|
+
|
|
385
|
+
assert.equal(router.validateRoute(route), true)
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
describe('#createChildRouter()', () => {
|
|
390
|
+
it('should create and return a child router', () => {
|
|
391
|
+
const router = new Router('/api', mockApp)
|
|
392
|
+
const child = router.createChildRouter('users')
|
|
393
|
+
|
|
394
|
+
assert.ok(child instanceof Router)
|
|
395
|
+
assert.equal(child.root, 'users')
|
|
396
|
+
assert.equal(child.parentRouter, router)
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
it('should add child to childRouters array', () => {
|
|
400
|
+
const router = new Router('/api', mockApp)
|
|
401
|
+
router.createChildRouter('users')
|
|
402
|
+
|
|
403
|
+
assert.equal(router.childRouters.length, 1)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('should pass routes to child router', () => {
|
|
407
|
+
const router = new Router('/api', mockApp)
|
|
408
|
+
const routes = [{ route: '/list', handlers: { get: () => {} } }]
|
|
409
|
+
const child = router.createChildRouter('users', routes)
|
|
410
|
+
|
|
411
|
+
assert.equal(child.routes.length, 1)
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it('should pass middleware to child router', () => {
|
|
415
|
+
const router = new Router('/api', mockApp)
|
|
416
|
+
const mw = () => {}
|
|
417
|
+
const child = router.createChildRouter('users', [], [mw])
|
|
418
|
+
|
|
419
|
+
assert.ok(child.routerMiddleware.includes(mw))
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
it('should pass handler middleware to child router', () => {
|
|
423
|
+
const router = new Router('/api', mockApp)
|
|
424
|
+
const mw = () => {}
|
|
425
|
+
const child = router.createChildRouter('users', [], [], [mw])
|
|
426
|
+
|
|
427
|
+
assert.ok(child.handlerMiddleware.includes(mw))
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it('should not create child after initialisation', () => {
|
|
431
|
+
const router = new Router('/api', mockApp)
|
|
432
|
+
router.init()
|
|
433
|
+
|
|
434
|
+
const result = router.createChildRouter('late')
|
|
435
|
+
|
|
436
|
+
assert.equal(result, router)
|
|
437
|
+
assert.equal(router.childRouters.length, 0)
|
|
438
|
+
})
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
describe('#warnOnInited()', () => {
|
|
442
|
+
it('should return false if not initialised', () => {
|
|
443
|
+
const router = new Router('/test', mockApp)
|
|
444
|
+
|
|
445
|
+
assert.equal(router.warnOnInited('test message'), false)
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it('should return true if initialised', () => {
|
|
449
|
+
const router = new Router('/test', mockApp)
|
|
450
|
+
router.init()
|
|
451
|
+
|
|
452
|
+
assert.equal(router.warnOnInited('test message'), true)
|
|
453
|
+
})
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
describe('#init()', () => {
|
|
457
|
+
it('should set _initialised to true', () => {
|
|
458
|
+
const router = new Router('/test', mockApp)
|
|
459
|
+
|
|
460
|
+
router.init()
|
|
461
|
+
|
|
462
|
+
assert.equal(router._initialised, true)
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it('should apply router middleware to express router', () => {
|
|
466
|
+
const mw = (req, res, next) => next()
|
|
467
|
+
const router = new Router('/test', mockApp, [], [mw])
|
|
468
|
+
|
|
469
|
+
router.init()
|
|
470
|
+
|
|
471
|
+
assert.ok(router.routerMiddleware.includes(mw))
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
it('should initialise child routers', () => {
|
|
475
|
+
const router = new Router('/api', mockApp)
|
|
476
|
+
const child = router.createChildRouter('users')
|
|
477
|
+
|
|
478
|
+
router.init()
|
|
479
|
+
|
|
480
|
+
assert.equal(child._initialised, true)
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
it('should register routes on the express router', () => {
|
|
484
|
+
const handler = (req, res) => res.json({})
|
|
485
|
+
const routes = [{ route: '/items', handlers: { get: handler } }]
|
|
486
|
+
const router = new Router('/test', mockApp, routes)
|
|
487
|
+
|
|
488
|
+
router.init()
|
|
489
|
+
|
|
490
|
+
assert.equal(router._initialised, true)
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
it('should not re-initialise if already initialised', () => {
|
|
494
|
+
const router = new Router('/test', mockApp)
|
|
495
|
+
router.init()
|
|
496
|
+
|
|
497
|
+
// Second call should be a no-op
|
|
498
|
+
router.init()
|
|
499
|
+
|
|
500
|
+
assert.equal(router._initialised, true)
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
it('should mount to parent Router via expressRouter.use', () => {
|
|
504
|
+
const parentRouter = new Router('/api', mockApp)
|
|
505
|
+
const childRouter = new Router('users', parentRouter)
|
|
506
|
+
|
|
507
|
+
childRouter.init()
|
|
508
|
+
|
|
509
|
+
assert.equal(childRouter._initialised, true)
|
|
510
|
+
})
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
describe('#flattenRouters()', () => {
|
|
514
|
+
it('should return array containing the router itself', () => {
|
|
515
|
+
const router = new Router('/api', mockApp)
|
|
516
|
+
const result = router.flattenRouters()
|
|
517
|
+
|
|
518
|
+
assert.ok(Array.isArray(result))
|
|
519
|
+
assert.ok(result.includes(router))
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
it('should include child routers', () => {
|
|
523
|
+
const router = new Router('/api', mockApp)
|
|
524
|
+
const child = router.createChildRouter('users')
|
|
525
|
+
|
|
526
|
+
const result = router.flattenRouters()
|
|
527
|
+
|
|
528
|
+
assert.ok(result.includes(router))
|
|
529
|
+
assert.ok(result.includes(child))
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
it('should include nested grandchild routers', () => {
|
|
533
|
+
const router = new Router('/api', mockApp)
|
|
534
|
+
const child = router.createChildRouter('v1')
|
|
535
|
+
const grandchild = child.createChildRouter('users')
|
|
536
|
+
|
|
537
|
+
const result = router.flattenRouters()
|
|
538
|
+
|
|
539
|
+
assert.ok(result.includes(router))
|
|
540
|
+
assert.ok(result.includes(child))
|
|
541
|
+
assert.ok(result.includes(grandchild))
|
|
542
|
+
})
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
describe('#log()', () => {
|
|
546
|
+
it('should not throw when called', () => {
|
|
547
|
+
const router = new Router('/test', mockApp)
|
|
548
|
+
|
|
549
|
+
assert.doesNotThrow(() => router.log('debug', 'test message'))
|
|
550
|
+
})
|
|
551
|
+
})
|
|
552
|
+
})
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, it, before } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { Hook } from 'adapt-authoring-core'
|
|
4
|
+
|
|
5
|
+
describe('ServerModule', () => {
|
|
6
|
+
let ServerModule
|
|
7
|
+
|
|
8
|
+
before(async () => {
|
|
9
|
+
ServerModule = (await import('../lib/ServerModule.js')).default
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
function createMockApp () {
|
|
13
|
+
return {
|
|
14
|
+
onReady: () => new Promise(() => {}),
|
|
15
|
+
logger: { log: () => {}, name: 'adapt-authoring-logger' },
|
|
16
|
+
dependencyloader: {
|
|
17
|
+
instances: { 'adapt-authoring-server': { url: 'http://localhost:5000' } },
|
|
18
|
+
moduleLoadedHook: new Hook()
|
|
19
|
+
},
|
|
20
|
+
config: {
|
|
21
|
+
get: (key) => {
|
|
22
|
+
const values = {
|
|
23
|
+
'ServerModule.host': 'localhost',
|
|
24
|
+
'ServerModule.port': 5000,
|
|
25
|
+
'ServerModule.trustProxy': true,
|
|
26
|
+
'ServerModule.url': null,
|
|
27
|
+
'ServerModule.verboseErrorLogging': false
|
|
28
|
+
}
|
|
29
|
+
return values[key]
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
errors: {
|
|
33
|
+
SERVER_START: { setData: (data) => ({ ...data, statusCode: 500 }) }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('constructor', () => {
|
|
39
|
+
it('should be defined', () => {
|
|
40
|
+
assert.ok(ServerModule)
|
|
41
|
+
assert.equal(typeof ServerModule, 'function')
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('#static()', () => {
|
|
46
|
+
it('should return express.static middleware', () => {
|
|
47
|
+
const instance = new ServerModule(createMockApp())
|
|
48
|
+
const middleware = instance.static('/public')
|
|
49
|
+
|
|
50
|
+
assert.equal(typeof middleware, 'function')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should accept options parameter', () => {
|
|
54
|
+
const instance = new ServerModule(createMockApp())
|
|
55
|
+
const middleware = instance.static('/public', { maxAge: 3600 })
|
|
56
|
+
|
|
57
|
+
assert.equal(typeof middleware, 'function')
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('properties', () => {
|
|
62
|
+
it('should have host property from config', () => {
|
|
63
|
+
const instance = new ServerModule(createMockApp())
|
|
64
|
+
|
|
65
|
+
assert.equal(instance.host, 'localhost')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should have port property from config', () => {
|
|
69
|
+
const instance = new ServerModule(createMockApp())
|
|
70
|
+
|
|
71
|
+
assert.equal(instance.port, 5000)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should return url from config when set', () => {
|
|
75
|
+
const app = createMockApp()
|
|
76
|
+
const origGet = app.config.get
|
|
77
|
+
app.config.get = (key) => {
|
|
78
|
+
if (key === 'ServerModule.url') return 'http://example.com'
|
|
79
|
+
return origGet(key)
|
|
80
|
+
}
|
|
81
|
+
const instance = new ServerModule(app)
|
|
82
|
+
|
|
83
|
+
assert.equal(instance.url, 'http://example.com')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should generate url from host and port when url config is not set', () => {
|
|
87
|
+
const instance = new ServerModule(createMockApp())
|
|
88
|
+
|
|
89
|
+
assert.equal(instance.url, 'localhost:5000')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should have isListening set to false initially', () => {
|
|
93
|
+
const instance = new ServerModule(createMockApp())
|
|
94
|
+
|
|
95
|
+
assert.equal(instance.isListening, false)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should have a listeningHook', () => {
|
|
99
|
+
const instance = new ServerModule(createMockApp())
|
|
100
|
+
|
|
101
|
+
assert.ok(instance.listeningHook instanceof Hook)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should have a requestHook', () => {
|
|
105
|
+
const instance = new ServerModule(createMockApp())
|
|
106
|
+
|
|
107
|
+
assert.ok(instance.requestHook instanceof Hook)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should have an expressApp', () => {
|
|
111
|
+
const instance = new ServerModule(createMockApp())
|
|
112
|
+
|
|
113
|
+
assert.equal(typeof instance.expressApp, 'function')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should have an api Router', () => {
|
|
117
|
+
const instance = new ServerModule(createMockApp())
|
|
118
|
+
|
|
119
|
+
assert.ok(instance.api)
|
|
120
|
+
assert.equal(instance.api.root, '/api')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should have a root Router', () => {
|
|
124
|
+
const instance = new ServerModule(createMockApp())
|
|
125
|
+
|
|
126
|
+
assert.ok(instance.root)
|
|
127
|
+
assert.equal(instance.root.root, '/')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('should set httpServer to undefined initially', () => {
|
|
131
|
+
const instance = new ServerModule(createMockApp())
|
|
132
|
+
|
|
133
|
+
assert.equal(instance.httpServer, undefined)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('should set isListening to false initially', () => {
|
|
137
|
+
const instance = new ServerModule(createMockApp())
|
|
138
|
+
|
|
139
|
+
assert.equal(instance.isListening, false)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should set trust proxy on express app', () => {
|
|
143
|
+
const instance = new ServerModule(createMockApp())
|
|
144
|
+
|
|
145
|
+
assert.equal(instance.expressApp.get('trust proxy'), true)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('should set view engine to hbs', () => {
|
|
149
|
+
const instance = new ServerModule(createMockApp())
|
|
150
|
+
|
|
151
|
+
assert.equal(instance.expressApp.get('view engine'), 'hbs')
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe('#close()', () => {
|
|
156
|
+
it('should resolve if httpServer is not set', async () => {
|
|
157
|
+
const instance = new ServerModule(createMockApp())
|
|
158
|
+
instance.httpServer = undefined
|
|
159
|
+
|
|
160
|
+
await assert.doesNotReject(() => instance.close())
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
})
|
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
import { describe, it, before } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { App } from 'adapt-authoring-core'
|
|
4
|
+
|
|
5
|
+
describe('ServerUtils', () => {
|
|
6
|
+
let ServerUtils
|
|
7
|
+
|
|
8
|
+
before(async () => {
|
|
9
|
+
const app = App.instance
|
|
10
|
+
app.logger = { log: () => {}, name: 'adapt-authoring-logger' }
|
|
11
|
+
app.dependencyloader.instances = app.dependencyloader.instances || {}
|
|
12
|
+
app.dependencyloader.instances['adapt-authoring-server'] = { url: 'http://localhost:5000' }
|
|
13
|
+
await app.onReady().catch(() => {})
|
|
14
|
+
process.exitCode = 0
|
|
15
|
+
|
|
16
|
+
ServerUtils = (await import('../lib/ServerUtils.js')).default
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('#addExistenceProps()', () => {
|
|
20
|
+
it('should set hasBody, hasParams, hasQuery to false for empty objects', () => {
|
|
21
|
+
const req = { method: 'POST', body: {}, params: {}, query: {} }
|
|
22
|
+
const res = {}
|
|
23
|
+
let nextCalled = false
|
|
24
|
+
const next = () => { nextCalled = true }
|
|
25
|
+
|
|
26
|
+
ServerUtils.addExistenceProps(req, res, next)
|
|
27
|
+
|
|
28
|
+
assert.equal(req.hasBody, false)
|
|
29
|
+
assert.equal(req.hasParams, false)
|
|
30
|
+
assert.equal(req.hasQuery, false)
|
|
31
|
+
assert.equal(nextCalled, true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should set hasBody, hasParams, hasQuery to true for populated objects', () => {
|
|
35
|
+
const req = { method: 'POST', body: { foo: 'bar' }, params: { id: '123' }, query: { search: 'test' } }
|
|
36
|
+
const res = {}
|
|
37
|
+
let nextCalled = false
|
|
38
|
+
const next = () => { nextCalled = true }
|
|
39
|
+
|
|
40
|
+
ServerUtils.addExistenceProps(req, res, next)
|
|
41
|
+
|
|
42
|
+
assert.equal(req.hasBody, true)
|
|
43
|
+
assert.equal(req.hasParams, true)
|
|
44
|
+
assert.equal(req.hasQuery, true)
|
|
45
|
+
assert.equal(nextCalled, true)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should remove undefined and null values from request objects', () => {
|
|
49
|
+
const req = { method: 'POST', body: { foo: 'bar', baz: undefined, qux: null }, params: {}, query: {} }
|
|
50
|
+
const res = {}
|
|
51
|
+
const next = () => {}
|
|
52
|
+
|
|
53
|
+
ServerUtils.addExistenceProps(req, res, next)
|
|
54
|
+
|
|
55
|
+
assert.equal(req.body.foo, 'bar')
|
|
56
|
+
assert.equal(req.body.baz, undefined)
|
|
57
|
+
assert.equal(req.body.qux, undefined)
|
|
58
|
+
assert.equal(req.hasBody, true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should clear body for GET requests', () => {
|
|
62
|
+
const req = { method: 'GET', body: { foo: 'bar' }, params: {}, query: {} }
|
|
63
|
+
const res = {}
|
|
64
|
+
const next = () => {}
|
|
65
|
+
|
|
66
|
+
ServerUtils.addExistenceProps(req, res, next)
|
|
67
|
+
|
|
68
|
+
assert.deepEqual(req.body, {})
|
|
69
|
+
assert.equal(req.hasBody, false)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should handle falsy attr values (missing body/params/query)', () => {
|
|
73
|
+
const req = { method: 'POST', body: null, params: undefined, query: false }
|
|
74
|
+
const res = {}
|
|
75
|
+
const next = () => {}
|
|
76
|
+
|
|
77
|
+
ServerUtils.addExistenceProps(req, res, next)
|
|
78
|
+
|
|
79
|
+
assert.equal(req.hasBody, true)
|
|
80
|
+
assert.equal(req.hasParams, true)
|
|
81
|
+
assert.equal(req.hasQuery, true)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('should mark has* false when all entries are null or undefined', () => {
|
|
85
|
+
const req = { method: 'POST', body: { a: null, b: undefined }, params: {}, query: {} }
|
|
86
|
+
const res = {}
|
|
87
|
+
const next = () => {}
|
|
88
|
+
|
|
89
|
+
ServerUtils.addExistenceProps(req, res, next)
|
|
90
|
+
|
|
91
|
+
assert.equal(req.hasBody, false)
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('#cacheRouteConfig()', () => {
|
|
96
|
+
it('should cache route config on request object', () => {
|
|
97
|
+
const routeConfig = { route: '/test', handlers: {}, internal: false }
|
|
98
|
+
const middleware = ServerUtils.cacheRouteConfig(routeConfig)
|
|
99
|
+
const req = {}
|
|
100
|
+
const res = {}
|
|
101
|
+
let nextCalled = false
|
|
102
|
+
const next = () => { nextCalled = true }
|
|
103
|
+
|
|
104
|
+
middleware(req, res, next)
|
|
105
|
+
|
|
106
|
+
assert.equal(req.routeConfig, routeConfig)
|
|
107
|
+
assert.equal(nextCalled, true)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should return a middleware function', () => {
|
|
111
|
+
const middleware = ServerUtils.cacheRouteConfig({})
|
|
112
|
+
|
|
113
|
+
assert.equal(typeof middleware, 'function')
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('#addErrorHandler()', () => {
|
|
118
|
+
it('should add sendError method to response object', () => {
|
|
119
|
+
const req = {}
|
|
120
|
+
const res = {}
|
|
121
|
+
let nextCalled = false
|
|
122
|
+
const next = () => { nextCalled = true }
|
|
123
|
+
|
|
124
|
+
ServerUtils.addErrorHandler(req, res, next)
|
|
125
|
+
|
|
126
|
+
assert.equal(typeof res.sendError, 'function')
|
|
127
|
+
assert.equal(nextCalled, true)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('sendError should send AdaptError as JSON with status code', () => {
|
|
131
|
+
const req = { translate: (error) => error.message }
|
|
132
|
+
const res = {}
|
|
133
|
+
const next = () => {}
|
|
134
|
+
|
|
135
|
+
ServerUtils.addErrorHandler(req, res, next)
|
|
136
|
+
|
|
137
|
+
let statusCode
|
|
138
|
+
let jsonData
|
|
139
|
+
res.status = (code) => { statusCode = code; return res }
|
|
140
|
+
res.json = (data) => { jsonData = data }
|
|
141
|
+
|
|
142
|
+
const adaptError = {
|
|
143
|
+
constructor: { name: 'AdaptError' },
|
|
144
|
+
statusCode: 404,
|
|
145
|
+
code: 'NOT_FOUND',
|
|
146
|
+
message: 'Not found'
|
|
147
|
+
}
|
|
148
|
+
res.sendError(adaptError)
|
|
149
|
+
|
|
150
|
+
assert.equal(statusCode, 404)
|
|
151
|
+
assert.equal(jsonData.code, 'NOT_FOUND')
|
|
152
|
+
assert.equal(jsonData.message, 'Not found')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('sendError should fall back to SERVER_ERROR for unknown errors', () => {
|
|
156
|
+
App.instance.errors = App.instance.errors || {}
|
|
157
|
+
App.instance.errors.SERVER_ERROR = {
|
|
158
|
+
constructor: { name: 'AdaptError' },
|
|
159
|
+
statusCode: 500,
|
|
160
|
+
code: 'SERVER_ERROR',
|
|
161
|
+
message: 'Internal server error'
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const req = { translate: (error) => error.message }
|
|
165
|
+
const res = {}
|
|
166
|
+
const next = () => {}
|
|
167
|
+
|
|
168
|
+
ServerUtils.addErrorHandler(req, res, next)
|
|
169
|
+
|
|
170
|
+
let statusCode
|
|
171
|
+
let jsonData
|
|
172
|
+
res.status = (code) => { statusCode = code; return res }
|
|
173
|
+
res.json = (data) => { jsonData = data }
|
|
174
|
+
|
|
175
|
+
res.sendError(new Error('something broke'))
|
|
176
|
+
|
|
177
|
+
assert.equal(statusCode, 500)
|
|
178
|
+
assert.equal(jsonData.code, 'SERVER_ERROR')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('sendError should look up known error codes on non-AdaptError', () => {
|
|
182
|
+
App.instance.errors = App.instance.errors || {}
|
|
183
|
+
App.instance.errors.CUSTOM_CODE = {
|
|
184
|
+
statusCode: 422,
|
|
185
|
+
code: 'CUSTOM_CODE',
|
|
186
|
+
message: 'Custom error'
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const req = { translate: (error) => error.message }
|
|
190
|
+
const res = {}
|
|
191
|
+
const next = () => {}
|
|
192
|
+
|
|
193
|
+
ServerUtils.addErrorHandler(req, res, next)
|
|
194
|
+
|
|
195
|
+
let statusCode
|
|
196
|
+
let jsonData
|
|
197
|
+
res.status = (code) => { statusCode = code; return res }
|
|
198
|
+
res.json = (data) => { jsonData = data }
|
|
199
|
+
|
|
200
|
+
const error = new Error('details')
|
|
201
|
+
error.code = 'CUSTOM_CODE'
|
|
202
|
+
res.sendError(error)
|
|
203
|
+
|
|
204
|
+
assert.equal(statusCode, 422)
|
|
205
|
+
assert.equal(jsonData.code, 'CUSTOM_CODE')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('sendError should include data field in response', () => {
|
|
209
|
+
const req = { translate: (error) => error.message }
|
|
210
|
+
const res = {}
|
|
211
|
+
const next = () => {}
|
|
212
|
+
|
|
213
|
+
ServerUtils.addErrorHandler(req, res, next)
|
|
214
|
+
|
|
215
|
+
let jsonData
|
|
216
|
+
res.status = () => res
|
|
217
|
+
res.json = (data) => { jsonData = data }
|
|
218
|
+
|
|
219
|
+
const adaptError = {
|
|
220
|
+
constructor: { name: 'AdaptError' },
|
|
221
|
+
statusCode: 400,
|
|
222
|
+
code: 'BAD_REQUEST',
|
|
223
|
+
message: 'Bad request',
|
|
224
|
+
data: { field: 'email' }
|
|
225
|
+
}
|
|
226
|
+
res.sendError(adaptError)
|
|
227
|
+
|
|
228
|
+
assert.deepEqual(jsonData.data, { field: 'email' })
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('sendError should use error.message when req.translate is not available', () => {
|
|
232
|
+
const req = {}
|
|
233
|
+
const res = {}
|
|
234
|
+
const next = () => {}
|
|
235
|
+
|
|
236
|
+
ServerUtils.addErrorHandler(req, res, next)
|
|
237
|
+
|
|
238
|
+
let jsonData
|
|
239
|
+
res.status = () => res
|
|
240
|
+
res.json = (data) => { jsonData = data }
|
|
241
|
+
|
|
242
|
+
const adaptError = {
|
|
243
|
+
constructor: { name: 'AdaptError' },
|
|
244
|
+
statusCode: 500,
|
|
245
|
+
code: 'ERR',
|
|
246
|
+
message: 'fallback message'
|
|
247
|
+
}
|
|
248
|
+
res.sendError(adaptError)
|
|
249
|
+
|
|
250
|
+
assert.equal(jsonData.message, 'fallback message')
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
describe('#mapHandler()', () => {
|
|
255
|
+
it('should return a function', () => {
|
|
256
|
+
const mockRouter = { map: { test: 'data' } }
|
|
257
|
+
const handler = ServerUtils.mapHandler(mockRouter)
|
|
258
|
+
|
|
259
|
+
assert.equal(typeof handler, 'function')
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('should respond with router map', () => {
|
|
263
|
+
const mockRouter = { map: { test: 'data' } }
|
|
264
|
+
const handler = ServerUtils.mapHandler(mockRouter)
|
|
265
|
+
const req = {}
|
|
266
|
+
let responseData = null
|
|
267
|
+
const res = {
|
|
268
|
+
json: (data) => { responseData = data }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
handler(req, res)
|
|
272
|
+
|
|
273
|
+
assert.deepEqual(responseData, { test: 'data' })
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
describe('#getAllRoutes()', () => {
|
|
278
|
+
it('should collect routes from router hierarchy', () => {
|
|
279
|
+
const mockRouter = {
|
|
280
|
+
path: '/api',
|
|
281
|
+
routes: [
|
|
282
|
+
{ route: '/users', handlers: { get: () => {}, post: () => {} } },
|
|
283
|
+
{ route: '/posts', handlers: { get: () => {} } }
|
|
284
|
+
],
|
|
285
|
+
flattenRouters: () => [mockRouter]
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const routeMap = ServerUtils.getAllRoutes(mockRouter)
|
|
289
|
+
|
|
290
|
+
assert.ok(routeMap instanceof Map)
|
|
291
|
+
assert.equal(routeMap.size, 2)
|
|
292
|
+
assert.ok(routeMap.has('/api/users'))
|
|
293
|
+
assert.ok(routeMap.has('/api/posts'))
|
|
294
|
+
assert.ok(routeMap.get('/api/users').has('GET'))
|
|
295
|
+
assert.ok(routeMap.get('/api/users').has('POST'))
|
|
296
|
+
assert.ok(routeMap.get('/api/posts').has('GET'))
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('should collect routes from multiple routers in hierarchy', () => {
|
|
300
|
+
const childRouter = {
|
|
301
|
+
path: '/api/v1',
|
|
302
|
+
routes: [
|
|
303
|
+
{ route: '/items', handlers: { get: () => {} } }
|
|
304
|
+
]
|
|
305
|
+
}
|
|
306
|
+
const parentRouter = {
|
|
307
|
+
path: '/api',
|
|
308
|
+
routes: [
|
|
309
|
+
{ route: '/health', handlers: { get: () => {} } }
|
|
310
|
+
],
|
|
311
|
+
flattenRouters: () => [parentRouter, childRouter]
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const routeMap = ServerUtils.getAllRoutes(parentRouter)
|
|
315
|
+
|
|
316
|
+
assert.equal(routeMap.size, 2)
|
|
317
|
+
assert.ok(routeMap.has('/api/health'))
|
|
318
|
+
assert.ok(routeMap.has('/api/v1/items'))
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it('should handle root path "/" by omitting it from the prefix', () => {
|
|
322
|
+
const mockRouter = {
|
|
323
|
+
path: '/',
|
|
324
|
+
routes: [
|
|
325
|
+
{ route: '/status', handlers: { get: () => {} } }
|
|
326
|
+
],
|
|
327
|
+
flattenRouters: () => [mockRouter]
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const routeMap = ServerUtils.getAllRoutes(mockRouter)
|
|
331
|
+
|
|
332
|
+
assert.ok(routeMap.has('/status'))
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('should return empty map for router with no routes', () => {
|
|
336
|
+
const mockRouter = {
|
|
337
|
+
path: '/api',
|
|
338
|
+
routes: [],
|
|
339
|
+
flattenRouters: () => [mockRouter]
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const routeMap = ServerUtils.getAllRoutes(mockRouter)
|
|
343
|
+
|
|
344
|
+
assert.equal(routeMap.size, 0)
|
|
345
|
+
})
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
describe('#methodNotAllowedHandler()', () => {
|
|
349
|
+
it('should return a middleware function', () => {
|
|
350
|
+
const mockRouter = {
|
|
351
|
+
path: '/api',
|
|
352
|
+
routes: [],
|
|
353
|
+
flattenRouters: () => [mockRouter]
|
|
354
|
+
}
|
|
355
|
+
const handler = ServerUtils.methodNotAllowedHandler(mockRouter)
|
|
356
|
+
|
|
357
|
+
assert.equal(typeof handler, 'function')
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('should call next() when route is not found', () => {
|
|
361
|
+
const mockRouter = {
|
|
362
|
+
path: '/api',
|
|
363
|
+
routes: [
|
|
364
|
+
{ route: '/users', handlers: { get: () => {} } }
|
|
365
|
+
],
|
|
366
|
+
flattenRouters: () => [mockRouter]
|
|
367
|
+
}
|
|
368
|
+
const handler = ServerUtils.methodNotAllowedHandler(mockRouter)
|
|
369
|
+
let nextCalled = false
|
|
370
|
+
|
|
371
|
+
handler({ method: 'GET', path: '/unknown', originalUrl: '/api/unknown' }, {}, () => { nextCalled = true })
|
|
372
|
+
|
|
373
|
+
assert.equal(nextCalled, true)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('should call next() when method matches', () => {
|
|
377
|
+
const mockRouter = {
|
|
378
|
+
path: '/api',
|
|
379
|
+
routes: [
|
|
380
|
+
{ route: '/users', handlers: { get: () => {} } }
|
|
381
|
+
],
|
|
382
|
+
flattenRouters: () => [mockRouter]
|
|
383
|
+
}
|
|
384
|
+
const handler = ServerUtils.methodNotAllowedHandler(mockRouter)
|
|
385
|
+
let nextCalled = false
|
|
386
|
+
let nextError = null
|
|
387
|
+
|
|
388
|
+
handler(
|
|
389
|
+
{ method: 'GET', path: '/api/users', originalUrl: '/api/users' },
|
|
390
|
+
{},
|
|
391
|
+
(err) => { nextCalled = true; nextError = err }
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
assert.equal(nextCalled, true)
|
|
395
|
+
assert.equal(nextError, undefined)
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it('should call next with METHOD_NOT_ALLOWED when path exists but method does not match', () => {
|
|
399
|
+
const mockError = { code: 'METHOD_NOT_ALLOWED' }
|
|
400
|
+
App.instance.errors = App.instance.errors || {}
|
|
401
|
+
App.instance.errors.METHOD_NOT_ALLOWED = { setData: () => mockError }
|
|
402
|
+
|
|
403
|
+
const mockRouter = {
|
|
404
|
+
path: '/api',
|
|
405
|
+
routes: [
|
|
406
|
+
{ route: '/users', handlers: { get: () => {}, post: () => {} } }
|
|
407
|
+
],
|
|
408
|
+
flattenRouters: () => [mockRouter]
|
|
409
|
+
}
|
|
410
|
+
const handler = ServerUtils.methodNotAllowedHandler(mockRouter)
|
|
411
|
+
let nextError = null
|
|
412
|
+
|
|
413
|
+
handler(
|
|
414
|
+
{ method: 'DELETE', path: '/api/users', originalUrl: '/api/users' },
|
|
415
|
+
{},
|
|
416
|
+
(err) => { nextError = err }
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
assert.equal(nextError.code, 'METHOD_NOT_ALLOWED')
|
|
420
|
+
})
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
describe('#generateRouterMap()', () => {
|
|
424
|
+
it('should generate a router map', () => {
|
|
425
|
+
const mockRouter = {
|
|
426
|
+
root: 'api',
|
|
427
|
+
path: '/api',
|
|
428
|
+
url: 'http://localhost:5000/api',
|
|
429
|
+
routes: [
|
|
430
|
+
{ route: '/test', handlers: { get: () => {} } }
|
|
431
|
+
],
|
|
432
|
+
childRouters: [],
|
|
433
|
+
flattenRouters: function () {
|
|
434
|
+
return [this]
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const map = ServerUtils.generateRouterMap(mockRouter)
|
|
439
|
+
|
|
440
|
+
assert.equal(typeof map, 'object')
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
it('should return empty object for router with no routes', () => {
|
|
444
|
+
const mockRouter = {
|
|
445
|
+
root: 'api',
|
|
446
|
+
path: '/api',
|
|
447
|
+
url: 'http://localhost:5000/api',
|
|
448
|
+
routes: [],
|
|
449
|
+
childRouters: [],
|
|
450
|
+
flattenRouters: function () {
|
|
451
|
+
return [this]
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const map = ServerUtils.generateRouterMap(mockRouter)
|
|
456
|
+
|
|
457
|
+
assert.deepEqual(map, {})
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
it('should include endpoint URLs and accepted methods', () => {
|
|
461
|
+
const mockRouter = {
|
|
462
|
+
root: 'api',
|
|
463
|
+
route: '/api',
|
|
464
|
+
path: '/api',
|
|
465
|
+
url: 'http://localhost:5000/api',
|
|
466
|
+
routes: [
|
|
467
|
+
{ route: '/users', handlers: { get: () => {}, post: () => {} }, meta: { get: { description: 'list users' } } }
|
|
468
|
+
],
|
|
469
|
+
childRouters: [],
|
|
470
|
+
parentRouter: null,
|
|
471
|
+
flattenRouters: function () {
|
|
472
|
+
return [this]
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const map = ServerUtils.generateRouterMap(mockRouter)
|
|
477
|
+
const keys = Object.keys(map)
|
|
478
|
+
|
|
479
|
+
assert.ok(keys.length > 0)
|
|
480
|
+
const endpoints = map[keys[0]]
|
|
481
|
+
assert.ok(Array.isArray(endpoints))
|
|
482
|
+
assert.equal(endpoints[0].url, 'http://localhost:5000/api/users')
|
|
483
|
+
assert.ok('get' in endpoints[0].accepted_methods)
|
|
484
|
+
assert.ok('post' in endpoints[0].accepted_methods)
|
|
485
|
+
assert.deepEqual(endpoints[0].accepted_methods.get, { description: 'list users' })
|
|
486
|
+
assert.deepEqual(endpoints[0].accepted_methods.post, {})
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
it('should use relative route keys for child routers', () => {
|
|
490
|
+
const childRouter = {
|
|
491
|
+
root: 'users',
|
|
492
|
+
path: '/api/users',
|
|
493
|
+
url: 'http://localhost:5000/api/users',
|
|
494
|
+
routes: [
|
|
495
|
+
{ route: '/:id', handlers: { get: () => {} } }
|
|
496
|
+
],
|
|
497
|
+
childRouters: [],
|
|
498
|
+
parentRouter: null
|
|
499
|
+
}
|
|
500
|
+
const mockRouter = {
|
|
501
|
+
root: 'api',
|
|
502
|
+
route: '/api',
|
|
503
|
+
path: '/api',
|
|
504
|
+
url: 'http://localhost:5000/api',
|
|
505
|
+
routes: [],
|
|
506
|
+
childRouters: [childRouter],
|
|
507
|
+
flattenRouters: function () {
|
|
508
|
+
return [this, childRouter]
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
childRouter.parentRouter = mockRouter
|
|
512
|
+
|
|
513
|
+
const map = ServerUtils.generateRouterMap(mockRouter)
|
|
514
|
+
const keys = Object.keys(map)
|
|
515
|
+
|
|
516
|
+
assert.ok(keys.some(k => k.includes('users')))
|
|
517
|
+
})
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
describe('#rootNotFoundHandler()', () => {
|
|
521
|
+
it('should respond with NOT_FOUND status code', () => {
|
|
522
|
+
App.instance.errors = App.instance.errors || {}
|
|
523
|
+
App.instance.errors.NOT_FOUND = { statusCode: 404 }
|
|
524
|
+
|
|
525
|
+
let statusCode
|
|
526
|
+
let endCalled = false
|
|
527
|
+
const res = {
|
|
528
|
+
status: (code) => { statusCode = code; return res },
|
|
529
|
+
end: () => { endCalled = true }
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
ServerUtils.rootNotFoundHandler({}, res)
|
|
533
|
+
|
|
534
|
+
assert.equal(statusCode, 404)
|
|
535
|
+
assert.equal(endCalled, true)
|
|
536
|
+
})
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
describe('#apiNotFoundHandler()', () => {
|
|
540
|
+
it('should call next with ENDPOINT_NOT_FOUND error', () => {
|
|
541
|
+
const mockError = { code: 'ENDPOINT_NOT_FOUND' }
|
|
542
|
+
App.instance.errors = App.instance.errors || {}
|
|
543
|
+
App.instance.errors.ENDPOINT_NOT_FOUND = { setData: () => mockError }
|
|
544
|
+
|
|
545
|
+
let nextArg
|
|
546
|
+
ServerUtils.apiNotFoundHandler(
|
|
547
|
+
{ originalUrl: '/api/missing', method: 'GET' },
|
|
548
|
+
{},
|
|
549
|
+
(err) => { nextArg = err }
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
assert.equal(nextArg.code, 'ENDPOINT_NOT_FOUND')
|
|
553
|
+
})
|
|
554
|
+
})
|
|
555
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock AbstractModule for testing
|
|
3
|
+
*/
|
|
4
|
+
class AbstractModuleMock {
|
|
5
|
+
constructor () {
|
|
6
|
+
this.app = {
|
|
7
|
+
onReady: () => new Promise(() => {}),
|
|
8
|
+
errors: {}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async init () {}
|
|
13
|
+
|
|
14
|
+
getConfig (key) {
|
|
15
|
+
const config = {
|
|
16
|
+
host: 'localhost',
|
|
17
|
+
port: 5000,
|
|
18
|
+
trustProxy: true,
|
|
19
|
+
url: null
|
|
20
|
+
}
|
|
21
|
+
return config[key]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
log () {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default AbstractModuleMock
|