adapt-authoring-server 0.0.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/.eslintignore +1 -0
- package/.eslintrc +14 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +55 -0
- package/.github/ISSUE_TEMPLATE/config.yml +1 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +22 -0
- package/.github/dependabot.yml +11 -0
- package/.github/pull_request_template.md +25 -0
- package/.github/workflows/new.yml +19 -0
- package/adapt-authoring.json +8 -0
- package/conf/config.schema.json +38 -0
- package/docs/server-requests.md +29 -0
- package/errors/errors.json +24 -0
- package/index.js +5 -0
- package/lib/Router.js +272 -0
- package/lib/ServerModule.js +153 -0
- package/lib/ServerUtils.js +191 -0
- package/lib/typedefs.js +57 -0
- package/package.json +22 -0
- package/tests/router.spec.js +142 -0
- package/tests/serverModule.spec.js +51 -0
package/.eslintignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
node_modules
|
package/.eslintrc
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: Bug Report
|
|
2
|
+
description: File a bug report
|
|
3
|
+
labels: ["bug"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Thanks for taking the time to fill out this bug report!
|
|
9
|
+
- type: textarea
|
|
10
|
+
id: description
|
|
11
|
+
attributes:
|
|
12
|
+
label: What happened?
|
|
13
|
+
description: Please describe the issue
|
|
14
|
+
validations:
|
|
15
|
+
required: true
|
|
16
|
+
- type: textarea
|
|
17
|
+
id: expected
|
|
18
|
+
attributes:
|
|
19
|
+
label: Expected behaviour
|
|
20
|
+
description: Tell us what should have happened
|
|
21
|
+
- type: textarea
|
|
22
|
+
id: repro-steps
|
|
23
|
+
attributes:
|
|
24
|
+
label: Steps to reproduce
|
|
25
|
+
description: Tell us how to reproduce the issue
|
|
26
|
+
validations:
|
|
27
|
+
required: true
|
|
28
|
+
- type: input
|
|
29
|
+
id: aat-version
|
|
30
|
+
attributes:
|
|
31
|
+
label: Authoring tool version
|
|
32
|
+
description: What version of the Adapt authoring tool are you running?
|
|
33
|
+
validations:
|
|
34
|
+
required: true
|
|
35
|
+
- type: input
|
|
36
|
+
id: fw-version
|
|
37
|
+
attributes:
|
|
38
|
+
label: Framework version
|
|
39
|
+
description: What version of the Adapt framework are you running?
|
|
40
|
+
- type: dropdown
|
|
41
|
+
id: browsers
|
|
42
|
+
attributes:
|
|
43
|
+
label: What browsers are you seeing the problem on?
|
|
44
|
+
multiple: true
|
|
45
|
+
options:
|
|
46
|
+
- Firefox
|
|
47
|
+
- Chrome
|
|
48
|
+
- Safari
|
|
49
|
+
- Microsoft Edge
|
|
50
|
+
- type: textarea
|
|
51
|
+
id: logs
|
|
52
|
+
attributes:
|
|
53
|
+
label: Relevant log output
|
|
54
|
+
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
|
55
|
+
render: sh
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
blank_issues_enabled: false
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: Request a new feature
|
|
3
|
+
labels: ["enhancement"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Thanks for taking the time to request a new feature in the Adapt authoring tool! The Adapt team will consider all new feature requests, but unfortunately cannot commit to every one.
|
|
9
|
+
- type: textarea
|
|
10
|
+
id: description
|
|
11
|
+
attributes:
|
|
12
|
+
label: Feature description
|
|
13
|
+
description: Please describe your feature request
|
|
14
|
+
validations:
|
|
15
|
+
required: true
|
|
16
|
+
- type: checkboxes
|
|
17
|
+
id: contribute
|
|
18
|
+
attributes:
|
|
19
|
+
label: Can you work on this feature?
|
|
20
|
+
description: If you are able to commit your own time to work on this feature, it will greatly increase the liklihood of it being implemented by the core dev team. Otherwise, it will be triaged and prioritised alongside the core team's current priorities.
|
|
21
|
+
options:
|
|
22
|
+
- label: I can contribute
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
+
# package ecosystems to update and where the package manifests are located.
|
|
3
|
+
# Please see the documentation for all configuration options:
|
|
4
|
+
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
5
|
+
|
|
6
|
+
version: 2
|
|
7
|
+
updates:
|
|
8
|
+
- package-ecosystem: "npm" # See documentation for possible values
|
|
9
|
+
directory: "/" # Location of package manifests
|
|
10
|
+
schedule:
|
|
11
|
+
interval: "weekly"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[//]: # (Please title your PR according to eslint commit conventions)
|
|
2
|
+
[//]: # (See https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-eslint#eslint-convention for details)
|
|
3
|
+
|
|
4
|
+
[//]: # (Add a link to the original issue)
|
|
5
|
+
|
|
6
|
+
[//]: # (Delete as appropriate)
|
|
7
|
+
### Fix
|
|
8
|
+
* A sentence describing each fix
|
|
9
|
+
|
|
10
|
+
### Update
|
|
11
|
+
* A sentence describing each udpate
|
|
12
|
+
|
|
13
|
+
### New
|
|
14
|
+
* A sentence describing each new feature
|
|
15
|
+
|
|
16
|
+
### Breaking
|
|
17
|
+
* A sentence describing each breaking change
|
|
18
|
+
|
|
19
|
+
[//]: # (List appropriate steps for testing if needed)
|
|
20
|
+
### Testing
|
|
21
|
+
1. Steps for testing
|
|
22
|
+
|
|
23
|
+
[//]: # (Mention any other dependencies)
|
|
24
|
+
|
|
25
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Add to main project
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issues:
|
|
5
|
+
types:
|
|
6
|
+
- opened
|
|
7
|
+
pull_request:
|
|
8
|
+
types:
|
|
9
|
+
- opened
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
add-to-project:
|
|
13
|
+
name: Add to main project
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/add-to-project@v0.1.0
|
|
17
|
+
with:
|
|
18
|
+
project-url: https://github.com/orgs/adapt-security/projects/5
|
|
19
|
+
github-token: ${{ secrets.PROJECTS_SECRET }}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"host": {
|
|
6
|
+
"description": "Name of the host machine the server is running from",
|
|
7
|
+
"type": "string"
|
|
8
|
+
},
|
|
9
|
+
"port": {
|
|
10
|
+
"description": "Port to be used for listening to incoming connections",
|
|
11
|
+
"type": ["number", "string"]
|
|
12
|
+
},
|
|
13
|
+
"url": {
|
|
14
|
+
"description": "URL the server can be accessed from",
|
|
15
|
+
"type": "string",
|
|
16
|
+
"format": "uri",
|
|
17
|
+
"_adapt": {
|
|
18
|
+
"isPublic": true
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"trustProxy": {
|
|
22
|
+
"description": "Whether to trust the client's x-Forwarded-For header for the request IP address. Only enable if using your own trusted reverse proxy",
|
|
23
|
+
"type": ["number", "boolean"],
|
|
24
|
+
"default": 0
|
|
25
|
+
},
|
|
26
|
+
"debugRequestTime": {
|
|
27
|
+
"description": "Will log the execution time of each request",
|
|
28
|
+
"type": "boolean",
|
|
29
|
+
"default": false
|
|
30
|
+
},
|
|
31
|
+
"verboseErrorLogging": {
|
|
32
|
+
"description": "Whether to log errors in their entirety",
|
|
33
|
+
"type": "boolean",
|
|
34
|
+
"default": false
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"required": ["host", "port", "url"]
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Handling server requests
|
|
2
|
+
This page gives you some pointers on how to handle incoming requests in a way that will reduce the work needed by you as a developer, and create consistent and easy-to-process responses.
|
|
3
|
+
|
|
4
|
+
## Extend the `AbstractApiModule` class
|
|
5
|
+
If you're building a non-trivial API (particularly one that uses the database), we highly recommend that you use `AbstractApiModule` as a base, as this includes a lot of boilerplate code and helper functions to make handling HTTP requests much easier. See [this page](writing-an-api) for more info on using the `AbstractApiModule` class.
|
|
6
|
+
|
|
7
|
+
## Use HTTP status codes
|
|
8
|
+
This may go without saying, but please stick to standardised HTTP response codes; they state your intentions and make it nice and easy for other devs to work with and react to.
|
|
9
|
+
|
|
10
|
+
See the [Mozilla Developer Network docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) for a full list of HTTP response status codes and what they mean.
|
|
11
|
+
|
|
12
|
+
## Use the built-in error handler
|
|
13
|
+
The Server module adds a `sendError` utility function to the ServerResponse object that's passed to every route handler in the stack. Making use of this in your own code will ensure errors are formatted in a consistent way.
|
|
14
|
+
|
|
15
|
+
Using the helper function is as simple as:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
async myHandler(req, res, next) {
|
|
19
|
+
try {
|
|
20
|
+
// do some stuff
|
|
21
|
+
} catch() {
|
|
22
|
+
res.sendError(e);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
See the Express.js documentation for information on the extra functions available:
|
|
28
|
+
- [Request](https://expressjs.com/en/4x/api.html#req)
|
|
29
|
+
- [Response](https://expressjs.com/en/4x/api.html#res)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ENDPOINT_NOT_FOUND": {
|
|
3
|
+
"data": {
|
|
4
|
+
"endpoint": "The missing endpoint",
|
|
5
|
+
"method": "The HTTP method"
|
|
6
|
+
},
|
|
7
|
+
"description": "API endpoint does not exist",
|
|
8
|
+
"statusCode": 404
|
|
9
|
+
},
|
|
10
|
+
"NO_NET_CONN": {
|
|
11
|
+
"data": {
|
|
12
|
+
"hostname": "The hostname we were trying to reach"
|
|
13
|
+
},
|
|
14
|
+
"description": "No network connection",
|
|
15
|
+
"statusCode": 400
|
|
16
|
+
},
|
|
17
|
+
"SERVER_START": {
|
|
18
|
+
"data": {
|
|
19
|
+
"error": "The error"
|
|
20
|
+
},
|
|
21
|
+
"description": "Failed to start server",
|
|
22
|
+
"statusCode": 500
|
|
23
|
+
}
|
|
24
|
+
}
|
package/index.js
ADDED
package/lib/Router.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import { App } from 'adapt-authoring-core'
|
|
3
|
+
import express from 'express'
|
|
4
|
+
import ServerUtils from './ServerUtils.js'
|
|
5
|
+
/**
|
|
6
|
+
* Handles the Express routing functionality
|
|
7
|
+
* @memberof server
|
|
8
|
+
*/
|
|
9
|
+
class Router {
|
|
10
|
+
/**
|
|
11
|
+
* If passing an {@link ExpressRouter} as the parentRouter, it is assumed that the Express Router is the top of the router 'heirarchy' (which will have an impact of some of the {@link Router} methods)
|
|
12
|
+
* @param {String} root Route API endpoint for this router
|
|
13
|
+
* @param {Router|ExpressRouter} parentRouter Parent to mount router
|
|
14
|
+
* @param {Array<Route>} routes Array of routes
|
|
15
|
+
*/
|
|
16
|
+
constructor (root, parentRouter, routes) {
|
|
17
|
+
/**
|
|
18
|
+
* The root route the router will be mounted at
|
|
19
|
+
* @type {String}
|
|
20
|
+
*/
|
|
21
|
+
this.root = root
|
|
22
|
+
/**
|
|
23
|
+
* Routes config
|
|
24
|
+
* @type {Array<Route>}
|
|
25
|
+
*/
|
|
26
|
+
this.routes = routes ?? []
|
|
27
|
+
/**
|
|
28
|
+
* Express router instance
|
|
29
|
+
* @type {external:ExpressRouter}
|
|
30
|
+
*/
|
|
31
|
+
this.expressRouter = express.Router()
|
|
32
|
+
/**
|
|
33
|
+
* Express router instance
|
|
34
|
+
* @type {ExpressApp|Router}
|
|
35
|
+
*/
|
|
36
|
+
this.parentRouter = parentRouter
|
|
37
|
+
/**
|
|
38
|
+
* List of sub-routers
|
|
39
|
+
* @type {Array<Router>}
|
|
40
|
+
*/
|
|
41
|
+
this.childRouters = []
|
|
42
|
+
/**
|
|
43
|
+
* Middleware stack to be added directly to the router
|
|
44
|
+
* @type {Array<Function>}
|
|
45
|
+
*/
|
|
46
|
+
this.routerMiddleware = [
|
|
47
|
+
ServerUtils.addErrorHandler
|
|
48
|
+
]
|
|
49
|
+
/**
|
|
50
|
+
* Middleware stack to be added before route handlers (useful if you need access to specific request attributes that don't exist when standard middleware runs)
|
|
51
|
+
* @type {Array<Function>}
|
|
52
|
+
*/
|
|
53
|
+
this.handlerMiddleware = [
|
|
54
|
+
ServerUtils.addExistenceProps,
|
|
55
|
+
ServerUtils.handleInternalRoutes
|
|
56
|
+
]
|
|
57
|
+
/** @ignore */this._initialised = false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns the map of routes attached to this router
|
|
62
|
+
* @type {Object}
|
|
63
|
+
*/
|
|
64
|
+
get map () {
|
|
65
|
+
return ServerUtils.generateRouterMap(this)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generates this router's path from its ancestors
|
|
70
|
+
* @type {String}
|
|
71
|
+
*/
|
|
72
|
+
get path () {
|
|
73
|
+
let p = ''
|
|
74
|
+
|
|
75
|
+
if (_.isString(this.parentRouter.path)) {
|
|
76
|
+
p += this.parentRouter.path
|
|
77
|
+
}
|
|
78
|
+
if (p[p.length - 1] !== '/' && this.root[0] !== '/') {
|
|
79
|
+
p += '/'
|
|
80
|
+
}
|
|
81
|
+
return p + this.root
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Returns the URL for the router
|
|
86
|
+
* @return {String} The URL
|
|
87
|
+
*/
|
|
88
|
+
get url () {
|
|
89
|
+
try {
|
|
90
|
+
const serverUrl = App.instance.dependencyloader.instances['adapt-authoring-server'].url
|
|
91
|
+
return serverUrl + this.path
|
|
92
|
+
} catch (e) {
|
|
93
|
+
this.log('error', e)
|
|
94
|
+
return ''
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Adds middleware to the router stack. Accepts multiple params.
|
|
100
|
+
* @param {...Function} func Middleware function(s) to be added
|
|
101
|
+
* @return {AbstractApiModule} This instance, for chaining
|
|
102
|
+
* @see https://expressjs.com/en/guide/using-middleware.html
|
|
103
|
+
*/
|
|
104
|
+
addMiddleware (...func) {
|
|
105
|
+
return this._addMiddleware(this.routerMiddleware, ...func)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Adds middleware to be called prior to any route handlers. Accepts multiple params. Useful if you need access to specific request attributes that don't exist when standard middleware runs.
|
|
110
|
+
* @param {...Function} func Middleware function(s) to be added
|
|
111
|
+
* @return {AbstractApiModule} This instance, for chaining
|
|
112
|
+
* @see https://expressjs.com/en/guide/using-middleware.html
|
|
113
|
+
*/
|
|
114
|
+
addHandlerMiddleware (...func) {
|
|
115
|
+
return this._addMiddleware(this.handlerMiddleware, ...func)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @ignore */ _addMiddleware (stack, ...func) {
|
|
119
|
+
if (func.length) {
|
|
120
|
+
this.warnOnInited('middleware may not be called before any route handlers')
|
|
121
|
+
func.forEach(f => _.isFunction(f) && !stack.includes(f) && stack.push(f))
|
|
122
|
+
}
|
|
123
|
+
return this
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Recursively gets middleware of the current router heirarchy
|
|
128
|
+
* @param {Router} router The current router (used when run recursively)
|
|
129
|
+
* @param {Array<function>} middleware Middleware function(s) (used when run recursively)
|
|
130
|
+
* @return {Array<Function>}
|
|
131
|
+
*/
|
|
132
|
+
getHandlerMiddleware (router = this, middleware = []) {
|
|
133
|
+
if (!(router instanceof Router)) {
|
|
134
|
+
return middleware
|
|
135
|
+
}
|
|
136
|
+
return _.uniq(this.getHandlerMiddleware(router.parentRouter, [...router.handlerMiddleware, ...middleware]))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Store route definition. Accepts multiple params.
|
|
141
|
+
* @param {...Route} route Config of route(s) to be added
|
|
142
|
+
* @return {AbstractApiModule} This instance, for chaining
|
|
143
|
+
*/
|
|
144
|
+
addRoute (...route) {
|
|
145
|
+
const inited = this.warnOnInited(`cannot set further routes (${this.path} ${route.map(r => r.route).join(', ')})`)
|
|
146
|
+
if (!inited && route.length) {
|
|
147
|
+
this.routes.push(...route.filter(this.validateRoute, this))
|
|
148
|
+
}
|
|
149
|
+
return this
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Function for filtering bad route configs
|
|
154
|
+
* @param {Route} r Route config
|
|
155
|
+
* @return {Boolean}
|
|
156
|
+
*/
|
|
157
|
+
validateRoute (r) {
|
|
158
|
+
const ePrefix = `invalid route config for ${this.route} router`
|
|
159
|
+
if (!_.isString(r.route)) {
|
|
160
|
+
this.log('warn', `${ePrefix}, route must be a string`)
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
163
|
+
if (!r.handlers) {
|
|
164
|
+
this.log('warn', `${ePrefix}, no route handlers defined`)
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
// handlers can be single function or array of functions
|
|
168
|
+
const allHandlersFuncs = Object.entries(r.handlers).every(([m, h]) => {
|
|
169
|
+
if (this.expressRouter[m] === undefined) {
|
|
170
|
+
this.log('warn', `${ePrefix}, ${m} must be a valid Express.js function`)
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
if (!_.isFunction(h) && !(_.isArray(h) && h.every(_.isFunction))) {
|
|
174
|
+
this.log('warn', `${ePrefix} ${m.toUpperCase()} ${r.route}, all route handlers must be functions`)
|
|
175
|
+
return false
|
|
176
|
+
}
|
|
177
|
+
return true
|
|
178
|
+
})
|
|
179
|
+
if (!allHandlersFuncs) {
|
|
180
|
+
return false
|
|
181
|
+
}
|
|
182
|
+
return true
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Creates and adds a sub-router to this router.
|
|
187
|
+
* @param {string} root The root of the child router
|
|
188
|
+
* @param {Array<Route>} routes Array of Routes to add
|
|
189
|
+
* @return {Router} The new router instance
|
|
190
|
+
*/
|
|
191
|
+
createChildRouter (root, routes) {
|
|
192
|
+
if (this.warnOnInited(`cannot create further child routers (${this.path}/${root})`)) {
|
|
193
|
+
return this
|
|
194
|
+
}
|
|
195
|
+
const router = new Router(root, this, routes)
|
|
196
|
+
|
|
197
|
+
this.childRouters.push(router)
|
|
198
|
+
|
|
199
|
+
this.log('debug', 'ADD_ROUTER', router.path)
|
|
200
|
+
|
|
201
|
+
return router
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Initialises the API
|
|
206
|
+
*/
|
|
207
|
+
init () {
|
|
208
|
+
if (this.warnOnInited(`(${this.path})`)) {
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
if (this.routerMiddleware.length) {
|
|
212
|
+
this.expressRouter.use(...this.routerMiddleware)
|
|
213
|
+
}
|
|
214
|
+
if (this.childRouters.length) {
|
|
215
|
+
this.childRouters.forEach(c => {
|
|
216
|
+
c.init()
|
|
217
|
+
this.expressRouter.use(c.root, c.expressRouter)
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
if (this.routes.length) {
|
|
221
|
+
this.routes.forEach(r => {
|
|
222
|
+
Object.entries(r.handlers).forEach(([method, handler]) => {
|
|
223
|
+
this.expressRouter[method](r.route, ServerUtils.cacheRouteConfig(r), ...this.getHandlerMiddleware(), handler)
|
|
224
|
+
this.log('debug', 'ADD_ROUTE', method.toUpperCase(), `${this.path !== '/' ? this.path : ''}${r.route}`)
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
// add to the parent stack
|
|
229
|
+
if (this.parentRouter instanceof Router) {
|
|
230
|
+
this.parentRouter.expressRouter.use(`/${this.root}`, this.expressRouter)
|
|
231
|
+
} else {
|
|
232
|
+
const route = this.root[0] !== '/' ? `/${this.root}` : this.root
|
|
233
|
+
this.parentRouter.use(route, this.expressRouter)
|
|
234
|
+
}
|
|
235
|
+
this._initialised = true
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Shortcut for checking Router has initialised, logging a warning if not
|
|
240
|
+
* @param {String} message Message to log on error
|
|
241
|
+
* @return {Boolean}
|
|
242
|
+
*/
|
|
243
|
+
warnOnInited (message) {
|
|
244
|
+
if (this._initialised) {
|
|
245
|
+
this.log('warn', `router has already initialised, ${message}`)
|
|
246
|
+
}
|
|
247
|
+
return this._initialised
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Creates an array defining the router inheritance hierarchy
|
|
252
|
+
* @param {Router} router The root router
|
|
253
|
+
* @return {Array}
|
|
254
|
+
*/
|
|
255
|
+
flattenRouters (router = this) {
|
|
256
|
+
return router.childRouters.reduce((a, c) => {
|
|
257
|
+
c.childRouters ? a.push(...this.flattenRouters(c)) : a.push(c)
|
|
258
|
+
return a
|
|
259
|
+
}, [router])
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Logs a message
|
|
264
|
+
* @param {String} level Level of log
|
|
265
|
+
* @param {...*} args Arguments to be logged
|
|
266
|
+
*/
|
|
267
|
+
log (level, ...args) {
|
|
268
|
+
App.instance.logger.log(level, this.constructor.name.toLowerCase(), ...args)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export default Router
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { AbstractModule, Hook } from 'adapt-authoring-core'
|
|
3
|
+
import Router from './Router.js'
|
|
4
|
+
import ServerUtils from './ServerUtils.js'
|
|
5
|
+
/**
|
|
6
|
+
* Adds an Express server to the authoring tool
|
|
7
|
+
* @memberof server
|
|
8
|
+
* @extends {AbstractModule}
|
|
9
|
+
*/
|
|
10
|
+
class ServerModule extends AbstractModule {
|
|
11
|
+
/** @override */
|
|
12
|
+
async init () {
|
|
13
|
+
/**
|
|
14
|
+
* The main Express Application
|
|
15
|
+
* @type {external:ExpressApp}
|
|
16
|
+
*/
|
|
17
|
+
this.expressApp = express()
|
|
18
|
+
/**
|
|
19
|
+
* The default/'root' router for the application
|
|
20
|
+
* @type {Router}
|
|
21
|
+
*/
|
|
22
|
+
this.root = new Router('/', this.expressApp)
|
|
23
|
+
/**
|
|
24
|
+
* The router which handles all APIs
|
|
25
|
+
* @type {Router}
|
|
26
|
+
*/
|
|
27
|
+
this.api = new Router('api', this.root)
|
|
28
|
+
/**
|
|
29
|
+
* Whether the HTTP server is listening for requests
|
|
30
|
+
* @type {Boolean}
|
|
31
|
+
*/
|
|
32
|
+
this.isListening = false
|
|
33
|
+
/**
|
|
34
|
+
* Hook invoked when the HTTP server is listening
|
|
35
|
+
* @type {Hook}
|
|
36
|
+
*/
|
|
37
|
+
this.listeningHook = new Hook()
|
|
38
|
+
/**
|
|
39
|
+
* Hook for interrupting requests
|
|
40
|
+
* @type {Hook}
|
|
41
|
+
*/
|
|
42
|
+
this.requestHook = new Hook({ mutable: true })
|
|
43
|
+
/**
|
|
44
|
+
* Reference to the HTTP server used by Express
|
|
45
|
+
* @type {external:HttpServer}
|
|
46
|
+
*/
|
|
47
|
+
this.httpServer = undefined
|
|
48
|
+
|
|
49
|
+
this.expressApp.set('trust proxy', this.getConfig('trustProxy'))
|
|
50
|
+
this.expressApp.set('view engine', 'hbs')
|
|
51
|
+
/**
|
|
52
|
+
* Need to wait for other modules to load before we properly initialise &
|
|
53
|
+
* start listening for incoming connections
|
|
54
|
+
*/
|
|
55
|
+
this.app.onReady().then(this.start.bind(this)).catch(e => this.log('error', e))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Starts the HTTP server
|
|
60
|
+
*/
|
|
61
|
+
async start () {
|
|
62
|
+
// Initialise the root router
|
|
63
|
+
this.root.init()
|
|
64
|
+
// Initialise the API router
|
|
65
|
+
this.api.expressRouter.get('/', ServerUtils.mapHandler(this.api).bind(this))
|
|
66
|
+
this.api.addMiddleware(
|
|
67
|
+
ServerUtils.debugRequestTime
|
|
68
|
+
)
|
|
69
|
+
this.api.init()
|
|
70
|
+
// add not-found handlers
|
|
71
|
+
this.api.expressRouter.use(ServerUtils.apiNotFoundHandler.bind(this))
|
|
72
|
+
this.root.expressRouter.use(ServerUtils.rootNotFoundHandler.bind(this))
|
|
73
|
+
// add generic error handling
|
|
74
|
+
this.expressApp.use(ServerUtils.genericErrorHandler.bind(this))
|
|
75
|
+
|
|
76
|
+
await this.listen()
|
|
77
|
+
this.log('info', `listening on ${this.port}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Server hostname
|
|
82
|
+
* @type {String}
|
|
83
|
+
*/
|
|
84
|
+
get host () {
|
|
85
|
+
return this.getConfig('host')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Port the app should listen on
|
|
90
|
+
* @type {String}
|
|
91
|
+
*/
|
|
92
|
+
get port () {
|
|
93
|
+
return this.getConfig('port')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The URL for the server from its config
|
|
98
|
+
* @type {String}
|
|
99
|
+
*/
|
|
100
|
+
get url () {
|
|
101
|
+
return this.getConfig('url') || `${this.getConfig('host')}:${this.port}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Middleware function to allow serving of static files
|
|
106
|
+
* @see https://expressjs.com/en/4x/api.html#express.static
|
|
107
|
+
* @param {String} root The root directory from which to serve static assets
|
|
108
|
+
* @param {Object} options Options to pass to the function
|
|
109
|
+
* @return {Function}
|
|
110
|
+
*/
|
|
111
|
+
static (root, options) {
|
|
112
|
+
return express.static(root, options)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Start the Express server (shortcut to the Express function of the same name).
|
|
117
|
+
* @see https://expressjs.com/en/4x/api.html#app.listen
|
|
118
|
+
* @param {function} func Callback function to be called on connection.
|
|
119
|
+
* @return {Promise} Resolves with the server instance
|
|
120
|
+
*/
|
|
121
|
+
listen () {
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
this.httpServer = this.expressApp.listen(this.port, this.host, () => {
|
|
124
|
+
this.isListening = true
|
|
125
|
+
this.listeningHook.invoke()
|
|
126
|
+
resolve(this.httpServer)
|
|
127
|
+
}).once('error', e => reject(this.app.errors.SERVER_START.setData({ error: e })))
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Stops the Express server
|
|
133
|
+
* @return {Promise}
|
|
134
|
+
*/
|
|
135
|
+
close () {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
if (!this.httpServer) {
|
|
138
|
+
this.log('warn', 'cannot stop server, it wasn\'t running!')
|
|
139
|
+
return resolve()
|
|
140
|
+
}
|
|
141
|
+
if (!this.isListening) return this.listeningHook.tap(this.close.bind(this))
|
|
142
|
+
else this.listeningHook.untap(this.close)
|
|
143
|
+
|
|
144
|
+
this.httpServer.close(() => {
|
|
145
|
+
this.httpServer = undefined
|
|
146
|
+
this.log('info', `no longer listening on ${this.port}`)
|
|
147
|
+
return resolve()
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export default ServerModule
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import { App } from 'adapt-authoring-core'
|
|
3
|
+
/**
|
|
4
|
+
* Server-related utilities
|
|
5
|
+
* @memberof server
|
|
6
|
+
*/
|
|
7
|
+
class ServerUtils {
|
|
8
|
+
/**
|
|
9
|
+
* Middleware for handling 404 errors on the API router
|
|
10
|
+
* @param {external:ExpressRequest} req
|
|
11
|
+
* @param {external:ExpressResponse} res
|
|
12
|
+
* @param {Function} next
|
|
13
|
+
*/
|
|
14
|
+
static apiNotFoundHandler (req, res, next) {
|
|
15
|
+
next(App.instance.errors.ENDPOINT_NOT_FOUND.setData({ endpoint: req.originalUrl, method: req.method }))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generic error handling middleware for the API router
|
|
20
|
+
* @param {Error} error
|
|
21
|
+
* @param {external:ExpressRequest} req
|
|
22
|
+
* @param {external:ExpressResponse} res
|
|
23
|
+
* @param {Function} next
|
|
24
|
+
*/
|
|
25
|
+
static genericErrorHandler (error, req, res, next) {
|
|
26
|
+
this.log('error', App.instance.lang.translate(undefined, error), this.getConfig('verboseErrorLogging') && error.stack ? error.stack : '')
|
|
27
|
+
res.sendError(error)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Middleware for handling 404 errors on the root router
|
|
32
|
+
* @param {external:ExpressRequest} req
|
|
33
|
+
* @param {external:ExpressResponse} res
|
|
34
|
+
*/
|
|
35
|
+
static rootNotFoundHandler (req, res) {
|
|
36
|
+
res.status(App.instance.errors.NOT_FOUND.statusCode).end()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Handler for returning an API map
|
|
41
|
+
* @param {Router} topRouter
|
|
42
|
+
* @return {Function} Middleware function
|
|
43
|
+
*/
|
|
44
|
+
static mapHandler (topRouter) {
|
|
45
|
+
return (req, res) => res.json(topRouter.map)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generates a map for a given router
|
|
50
|
+
* @param {Router} topRouter
|
|
51
|
+
* @return {Object} The route map
|
|
52
|
+
*/
|
|
53
|
+
static generateRouterMap (topRouter) {
|
|
54
|
+
return topRouter.flattenRouters()
|
|
55
|
+
.sort((a, b) => a.root.localeCompare(b.root))
|
|
56
|
+
.reduce((m, r) => {
|
|
57
|
+
const key = `${getRelativeRoute(topRouter, r)}endpoints`
|
|
58
|
+
const endpoints = getEndpoints(r)
|
|
59
|
+
return endpoints.length ? { ...m, [key]: endpoints } : m
|
|
60
|
+
}, {})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Adds extra properties to the request object to allow for easy translations
|
|
65
|
+
* @param {Function} next
|
|
66
|
+
*/
|
|
67
|
+
static addErrorHandler (req, res, next) {
|
|
68
|
+
res.sendError = error => {
|
|
69
|
+
if (error.constructor.name !== 'AdaptError') {
|
|
70
|
+
const e = App.instance.errors[error.code]
|
|
71
|
+
if (e) {
|
|
72
|
+
if (error.statusCode) e.statusCode = error.statusCode
|
|
73
|
+
e.error = error.message
|
|
74
|
+
error = e
|
|
75
|
+
} else {
|
|
76
|
+
error = App.instance.errors.SERVER_ERROR
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
res
|
|
80
|
+
.status(error.statusCode)
|
|
81
|
+
.json({ code: error.code, message: req.translate?.(error) ?? error.message })
|
|
82
|
+
}
|
|
83
|
+
next()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Adds logs for debugging each request time
|
|
88
|
+
* @param {external:ExpressRequest} req
|
|
89
|
+
* @param {external:ExpressResponse} res
|
|
90
|
+
* @param {Function} next
|
|
91
|
+
*/
|
|
92
|
+
static async debugRequestTime (req, res, next) {
|
|
93
|
+
const server = await App.instance.waitForModule('server')
|
|
94
|
+
if (server.getConfig('debugRequestTime')) {
|
|
95
|
+
const start = new Date()
|
|
96
|
+
res.on('finish', () => server.log('debug', 'REQUEST_DURATION', req.method, req.originalUrl, new Date() - start))
|
|
97
|
+
}
|
|
98
|
+
next()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Adds extra properties to the request object to allow for easy existence checking of common request objects
|
|
103
|
+
* @param {external:ExpressRequest} req
|
|
104
|
+
* @param {external:ExpressResponse} res
|
|
105
|
+
* @param {Function} next
|
|
106
|
+
* @example
|
|
107
|
+
* "IMPORTANT NOTE: body data is completely ignored for GET requests, any code
|
|
108
|
+
* requiring it should switch to use POST."
|
|
109
|
+
*
|
|
110
|
+
* let req = { 'params': { 'foo':'bar' }, 'query': {}, 'body': {} };
|
|
111
|
+
* req.hasParams // true
|
|
112
|
+
* req.hasQuery // false
|
|
113
|
+
* req.hasBody // false
|
|
114
|
+
*/
|
|
115
|
+
static addExistenceProps (req, res, next) {
|
|
116
|
+
if (req.method === 'GET') {
|
|
117
|
+
req.body = {}
|
|
118
|
+
}
|
|
119
|
+
const storeVal = (key, exists) => {
|
|
120
|
+
req[`has${_.capitalize(key)}`] = exists
|
|
121
|
+
}
|
|
122
|
+
['body', 'params', 'query'].forEach(attr => {
|
|
123
|
+
if (!req[attr]) {
|
|
124
|
+
return storeVal(attr, true)
|
|
125
|
+
}
|
|
126
|
+
const entries = Object.entries(req[attr])
|
|
127
|
+
let deleted = 0
|
|
128
|
+
if (entries.length === 0) {
|
|
129
|
+
return storeVal(attr, false)
|
|
130
|
+
}
|
|
131
|
+
entries.forEach(([key, val]) => {
|
|
132
|
+
if (val === undefined || val === null) {
|
|
133
|
+
delete req[attr][key]
|
|
134
|
+
deleted++
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
storeVal(attr, deleted < entries.length)
|
|
138
|
+
})
|
|
139
|
+
next()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Handles restriction of routes marked as internal
|
|
144
|
+
* @param {external:ExpressRequest} req
|
|
145
|
+
* @param {external:ExpressResponse} res
|
|
146
|
+
* @param {Function} next
|
|
147
|
+
*/
|
|
148
|
+
static async handleInternalRoutes (req, res, next) {
|
|
149
|
+
const server = await App.instance.waitForModule('server')
|
|
150
|
+
const isInternalIp = server.getConfig('host') === req.ip || req.ip === '127.0.0.1' || req.ip === '::1'
|
|
151
|
+
if (req.routeConfig.internal && !isInternalIp) {
|
|
152
|
+
return next(App.instance.errors.UNAUTHORISED.setData({ url: req.originalUrl, method: req.method }))
|
|
153
|
+
}
|
|
154
|
+
next()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Caches the route config on the incoming request
|
|
159
|
+
* @param {Route} routeConfig
|
|
160
|
+
* @return {Function}
|
|
161
|
+
*/
|
|
162
|
+
static cacheRouteConfig (routeConfig) {
|
|
163
|
+
return (req, res, next) => {
|
|
164
|
+
req.routeConfig = routeConfig
|
|
165
|
+
next()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** @ignore */ function getEndpoints (r) {
|
|
170
|
+
return r.routes.map(route => {
|
|
171
|
+
return {
|
|
172
|
+
url: `${r.url}${route.route}`,
|
|
173
|
+
accepted_methods: Object.keys(route.handlers).reduce((memo, method) => {
|
|
174
|
+
return {
|
|
175
|
+
...memo,
|
|
176
|
+
[method]: route?.meta?.[method] ?? {}
|
|
177
|
+
}
|
|
178
|
+
}, {})
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
/** @ignore */ function getRelativeRoute (relFrom, relTo) {
|
|
183
|
+
if (relFrom === relTo) {
|
|
184
|
+
return `${relFrom.route}_`
|
|
185
|
+
}
|
|
186
|
+
let route = ''
|
|
187
|
+
for (let r = relTo; r !== relFrom; r = r.parentRouter) route = `${r.root}_${route}`
|
|
188
|
+
return route
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export default ServerUtils
|
package/lib/typedefs.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file exists to define the below types for documentation purposes.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Built-in HTTP server
|
|
6
|
+
* @memberof server
|
|
7
|
+
* @external HttpServer
|
|
8
|
+
* @see {@link https://nodejs.org/api/http.html#http_class_http_server}
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Express.js top-level application
|
|
12
|
+
* @memberof server
|
|
13
|
+
* @external ExpressApp
|
|
14
|
+
* @see {@link https://expressjs.com/en/4x/api.html#app}
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Express.js HTTP router
|
|
18
|
+
* @memberof server
|
|
19
|
+
* @external ExpressRouter
|
|
20
|
+
* @see {@link https://expressjs.com/en/4x/api.html#router}
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Express.js HTTP request
|
|
24
|
+
* @memberof server
|
|
25
|
+
* @external ExpressRequest
|
|
26
|
+
* @see {@link https://expressjs.com/en/4x/api.html#req}
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Express.js HTTP response
|
|
30
|
+
* @memberof server
|
|
31
|
+
* @external ExpressResponse
|
|
32
|
+
* @see {@link https://nodejs.org/api/http.html#http_class_http_serverresponse}
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Defines how an individual API route should be handled
|
|
36
|
+
* @memberof server
|
|
37
|
+
* @typedef {Object} Route
|
|
38
|
+
* @property {String} route The name of the api (this will be used as the API endpoint)
|
|
39
|
+
* @property {Object} handlers Object mapping HTTP methods to request handler functions. Note: Any HTTP methods not specified in `handlers` will not be exposed.
|
|
40
|
+
* @property {Array<Function>|Function} [handlers.post] POST handlers for the route
|
|
41
|
+
* @property {Array<Function>|Function} [handlers.get] GET handlers for the route
|
|
42
|
+
* @property {Array<Function>|Function} [handlers.put] PUT handlers for the route
|
|
43
|
+
* @property {Array<Function>|Function} [handlers.delete] DELETE handlers for the route
|
|
44
|
+
* @example
|
|
45
|
+
* {
|
|
46
|
+
* route: '/:id?',
|
|
47
|
+
* handlers: {
|
|
48
|
+
* // can be an array of middleware/handlers
|
|
49
|
+
* post: [beforePost, handlePostRequest, afterPost],
|
|
50
|
+
* // or an individual function
|
|
51
|
+
* get: getRequest,
|
|
52
|
+
* put: putRequest,
|
|
53
|
+
* // or an in-line function
|
|
54
|
+
* delete: (req, res, next) => { next(); }
|
|
55
|
+
* }
|
|
56
|
+
* }
|
|
57
|
+
*/
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "adapt-authoring-server",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Provides an Express application routing and more",
|
|
5
|
+
"homepage": "https://github.com/adapt-security/adapt-authoring-server",
|
|
6
|
+
"license": "GPL-3.0",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"repository": "github:adapt-security/adapt-authoring-server",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"express": "^5.1.0",
|
|
12
|
+
"hbs": "^4.2.0",
|
|
13
|
+
"lodash": "^4.17.21"
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"adapt-authoring-core": "github:adapt-security/adapt-authoring-core"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"eslint": "^9.12.0",
|
|
20
|
+
"standard": "^17.1.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const { App } = require('adapt-authoring-core');
|
|
2
|
+
const express = require('express');
|
|
3
|
+
const Router = require('../lib/Router');
|
|
4
|
+
const should = require('should');
|
|
5
|
+
const supertest = require('supertest');
|
|
6
|
+
|
|
7
|
+
describe('Server router', function() {
|
|
8
|
+
before(function() {
|
|
9
|
+
this.createRouter = (r = 'test') => new Router(r, express());
|
|
10
|
+
this.testRequest = (router, done, expectedStatus = 200) => {
|
|
11
|
+
const l = router.parentRouter.listen(undefined, function() {
|
|
12
|
+
supertest(router.parentRouter)
|
|
13
|
+
.get(`${router.path}`)
|
|
14
|
+
.expect(expectedStatus)
|
|
15
|
+
.end((e, res) => l.close(done))
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
describe('#addRoute()', function() {
|
|
20
|
+
const r1 = { route: 'test1', handlers: { get: () => {} } };
|
|
21
|
+
const r2 = { route: 'test2', handlers: { get: () => {} } };
|
|
22
|
+
const r3 = { route: 'test3', handlers: { get: () => {} } };
|
|
23
|
+
it('should add a route function to the stack', function() {
|
|
24
|
+
const router = this.createRouter();
|
|
25
|
+
router.addRoute(r1);
|
|
26
|
+
router.routes.should.containEql(r1);
|
|
27
|
+
});
|
|
28
|
+
it('should add multiple routes to the stack', function() {
|
|
29
|
+
const router = this.createRouter();
|
|
30
|
+
router.addRoute(r1, r2);
|
|
31
|
+
router.routes.should.containEql(r2);
|
|
32
|
+
});
|
|
33
|
+
it('should return router reference so as to be chainable', function() {
|
|
34
|
+
const router = this.createRouter();
|
|
35
|
+
const r = router.addRoute(r2);
|
|
36
|
+
r.should.equal(router);
|
|
37
|
+
});
|
|
38
|
+
it('should call route handler on client request', function(done) {
|
|
39
|
+
const router = this.createRouter();
|
|
40
|
+
router.addRoute({
|
|
41
|
+
route: '/',
|
|
42
|
+
handlers: { get: (req, res, next) => res.end() }
|
|
43
|
+
});
|
|
44
|
+
router.init();
|
|
45
|
+
this.testRequest(router, done);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('#addMiddleware()', function() {
|
|
49
|
+
const m1 = (req, res, next) => next();
|
|
50
|
+
const m2 = (req, res, next) => next();
|
|
51
|
+
it('should add a middleware function to the stack', function() {
|
|
52
|
+
const router = this.createRouter();
|
|
53
|
+
router.addMiddleware(m1);
|
|
54
|
+
router.middleware.should.containEql(m1);
|
|
55
|
+
});
|
|
56
|
+
it('should add multiple middleware functions to the stack', function() {
|
|
57
|
+
const router = this.createRouter();
|
|
58
|
+
router.addMiddleware(m1, m2);
|
|
59
|
+
router.middleware.should.containEql(m2);
|
|
60
|
+
});
|
|
61
|
+
it('should return router reference so as to be chainable', function() {
|
|
62
|
+
const router = this.createRouter();
|
|
63
|
+
const r = router.addMiddleware(m2);
|
|
64
|
+
r.should.equal(router);
|
|
65
|
+
});
|
|
66
|
+
it('should call custom middleware on client request', function(done) {
|
|
67
|
+
const router = this.createRouter();
|
|
68
|
+
router.addRoute({
|
|
69
|
+
route: '/',
|
|
70
|
+
handlers: { get: (req, res, next) => res.status(500).end() }
|
|
71
|
+
});
|
|
72
|
+
router.addMiddleware((req, res, next) => res.status(200).end());
|
|
73
|
+
router.init();
|
|
74
|
+
this.testRequest(router, done);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe('#createChildRouter()', function() {
|
|
78
|
+
let parent, child;
|
|
79
|
+
before(function() {
|
|
80
|
+
parent = this.createRouter();
|
|
81
|
+
child = parent.createChildRouter('child');
|
|
82
|
+
});
|
|
83
|
+
it('should return a router instance', function() {
|
|
84
|
+
child.should.be.instanceof(Router);
|
|
85
|
+
});
|
|
86
|
+
it('should expose specified route', function() {
|
|
87
|
+
child.route.should.equal('child');
|
|
88
|
+
});
|
|
89
|
+
it('should be added to child routers', function() {
|
|
90
|
+
parent.childRouters.should.containEql(child);
|
|
91
|
+
});
|
|
92
|
+
it('should reference current router as parent', function() {
|
|
93
|
+
child.parentRouter.should.equal(parent);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('#path()', function() {
|
|
97
|
+
it('should generate the endpoint of the router', function() {
|
|
98
|
+
const child = new Router('child', new Router('parent', {}));
|
|
99
|
+
child.path.should.equal('/parent/child');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('#handleRequest()', function() {
|
|
103
|
+
it('should invoke listeners to Server#requestHook', function(done) {
|
|
104
|
+
let isDone = false;
|
|
105
|
+
App.instance.getModule('server').requestHook.tap(() => {
|
|
106
|
+
if(!isDone) done();
|
|
107
|
+
isDone = true;
|
|
108
|
+
});
|
|
109
|
+
const router = this.createRouter();
|
|
110
|
+
router.addRoute({
|
|
111
|
+
route: '/',
|
|
112
|
+
handlers: { get: (req, res, next) => {} }
|
|
113
|
+
});
|
|
114
|
+
router.init();
|
|
115
|
+
this.testRequest(router, () => {}, 500);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe('#genericNotFoundHandler()', function() {
|
|
119
|
+
it('should return 404 HTTP status', function(done) {
|
|
120
|
+
const router = this.createRouter();
|
|
121
|
+
router.addRoute({
|
|
122
|
+
route: '/',
|
|
123
|
+
handlers: { get: (req, res, next) => res.end() }
|
|
124
|
+
});
|
|
125
|
+
router.addMiddleware(router.genericNotFoundHandler);
|
|
126
|
+
router.init();
|
|
127
|
+
this.testRequest(router, done, 404);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe('#genericErrorHandler()', function() {
|
|
131
|
+
it('should return error HTTP status', function(done) {
|
|
132
|
+
const router = this.createRouter();
|
|
133
|
+
router.addRoute({
|
|
134
|
+
route: '/',
|
|
135
|
+
handlers: { get: (req, res, next) => next(new Error('Something bad happened')) }
|
|
136
|
+
});
|
|
137
|
+
router.addMiddleware(router.genericErrorHandler);
|
|
138
|
+
router.init();
|
|
139
|
+
this.testRequest(router, done, 500);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const { Hook } = require('adapt-authoring-core');
|
|
2
|
+
const Router = require('../lib/Router');
|
|
3
|
+
const ServerModule = require('../lib/ServerModule');
|
|
4
|
+
const should = require('should');
|
|
5
|
+
const supertest = require('supertest');
|
|
6
|
+
|
|
7
|
+
describe('Server module', function() {
|
|
8
|
+
before(function() {
|
|
9
|
+
this.server = new ServerModule(global.ADAPT.app, { name: require('../package.json').name });
|
|
10
|
+
});
|
|
11
|
+
describe('#constructor()', function() {
|
|
12
|
+
it('should expose the Express application', function() {
|
|
13
|
+
false.should.be.true();
|
|
14
|
+
});
|
|
15
|
+
it('should expose a `root` Router', function() {
|
|
16
|
+
this.server.root.should.be.an.instanceof(Router);
|
|
17
|
+
});
|
|
18
|
+
it('should expose an `api` Router', function() {
|
|
19
|
+
this.server.api.should.be.an.instanceof(Router);
|
|
20
|
+
});
|
|
21
|
+
it('should expose a hook to modify requests', function() {
|
|
22
|
+
this.server.requestHook.should.be.an.instanceof(Hook);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe('#url()', function() {
|
|
26
|
+
it('should return the URL of the server', function() {
|
|
27
|
+
this.server.url.should.be.a.String();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
describe('#static()', function() {
|
|
31
|
+
it('should expose Express#static', function() {
|
|
32
|
+
false.should.be.true();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe('#listen()', function() {
|
|
36
|
+
it('should accept requests on the specified URL/port', function(done) {
|
|
37
|
+
supertest(this.server.expressApp)
|
|
38
|
+
.get(`${this.server.api.path}`)
|
|
39
|
+
.expect(200)
|
|
40
|
+
.end(done);
|
|
41
|
+
});
|
|
42
|
+
it('should not accept requests on unspecified URLs/ports', function(done) {
|
|
43
|
+
false.should.be.true();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
describe('#close()', function() {
|
|
47
|
+
it('should stop accepting requests', function() {
|
|
48
|
+
false.should.be.true();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|