@vida-global/core 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/npm-publish.yml +1 -1
- package/.github/workflows/npm-test.yml +1 -1
- package/lib/logger/index.js +4 -1
- package/lib/server/README.md +30 -8
- package/lib/server/server.js +56 -31
- package/lib/server/serverController.js +126 -30
- package/package.json +3 -3
- package/test/server/server.test.js +1 -1
- package/test/server/serverController.test.js +86 -93
package/lib/logger/index.js
CHANGED
package/lib/server/README.md
CHANGED
|
@@ -118,9 +118,34 @@ Additional options can be passed to `toApiResponse` by explicitly calling `rende
|
|
|
118
118
|
await this.render(response, {includeTitle: true});
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
### Other Status Codes
|
|
122
|
+
There is no need to set `statusCode` directly as there are helper methods for all statuses.
|
|
123
|
+
```
|
|
124
|
+
201 - this.renderCreationSuccessful
|
|
125
|
+
202 - this.renderAccepted
|
|
126
|
+
204 - this.renderNoConent
|
|
127
|
+
301 - this.renderMovedPermanently
|
|
128
|
+
302 - this.renderFound
|
|
129
|
+
304 - this.renderNotModified
|
|
130
|
+
307 - this.renderTemporaryRedirect
|
|
131
|
+
308 - this.renderPermanentRedirect
|
|
132
|
+
401 - this.renderUnauthorizedResponse
|
|
133
|
+
402 - this.renderPaymentRequired
|
|
134
|
+
403 - this.renderForbiddenResponse
|
|
135
|
+
404 - this.renderNotFoundResponse
|
|
136
|
+
405 - this.renderMethodNotAllowed
|
|
137
|
+
408 - this.renderRequestTimeout
|
|
138
|
+
409 - this.renderConflictResponse
|
|
139
|
+
410 - this.renderGone
|
|
140
|
+
413 - this.renderContentTooLarge
|
|
141
|
+
415 - this.renderUnsupportedMediaType
|
|
142
|
+
422 - this.renderUnprocessableContent
|
|
143
|
+
423 - this.renderLocked
|
|
144
|
+
429 - this.renderTooManyRequests
|
|
145
|
+
```
|
|
146
|
+
|
|
121
147
|
|
|
122
148
|
## Error handling
|
|
123
|
-
There is no need to set `statusCode` directly as there are helper methods for all statuses.
|
|
124
149
|
HTTP 400
|
|
125
150
|
```
|
|
126
151
|
await this.renderErrors("Something bad happened", {password: ['must be > 8 characters', 'must include numbers']})
|
|
@@ -134,19 +159,16 @@ await this.renderErrors({password: ['must be > 8 characters', 'must include numb
|
|
|
134
159
|
```
|
|
135
160
|
HTTP 401
|
|
136
161
|
```
|
|
137
|
-
await this.
|
|
162
|
+
await this.renderUnauthorized("Nope")
|
|
138
163
|
// renders {data: {message: "Nope"}, status: "unauthorized"}
|
|
139
164
|
```
|
|
140
165
|
HTTP 403
|
|
141
166
|
```
|
|
142
|
-
await this.
|
|
167
|
+
await this.renderForbidden("You shall not pass")
|
|
143
168
|
// renders {data: {message: "You shall not pass"}, status: "forbidden"}
|
|
144
169
|
```
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
await this.renderNotFoundResponse("Whoops")
|
|
148
|
-
// renders {data: {message: "Whoops"}, status: "not found"}
|
|
149
|
-
```
|
|
170
|
+
* _Other statuses detailed above._
|
|
171
|
+
|
|
150
172
|
Throwing any of `this.Errors.Authorization(msg)`, `this.Errors.Forbidden(msg)`, `this.Errors.NotFound(msg)`, `this.Errors.Validation(msg, errors)` from anywhere in the code will trigger the corresponding error handler. All other errors will generate a 500 error.
|
|
151
173
|
|
|
152
174
|
|
package/lib/server/server.js
CHANGED
|
@@ -106,40 +106,17 @@ class VidaServer {
|
|
|
106
106
|
|
|
107
107
|
get loggingMiddleware() {
|
|
108
108
|
return httpLogger({
|
|
109
|
-
logger:
|
|
110
|
-
customLogLevel:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// Format the request log as a string
|
|
117
|
-
req: (req) => `Request: ${req.method} ${req.url}`,
|
|
118
|
-
// Format the response log as a string
|
|
119
|
-
res: (res) => `statusCode=${res.statusCode}, responseTime=${res.get('X-Response-Time')}`,
|
|
120
|
-
},
|
|
121
|
-
wrapSerializers: false,
|
|
109
|
+
logger: this.middlewareLogger,
|
|
110
|
+
customLogLevel: this.requestLogLevel.bind(this),
|
|
111
|
+
customSuccessMessage: this.requestLogMessage.bind(this),
|
|
112
|
+
customErrorMessage: this.requestLogMessage.bind(this),
|
|
113
|
+
customErrorObject: this.requestLogDetails.bind(this),
|
|
114
|
+
customSuccessObject: this.requestLogDetails.bind(this),
|
|
115
|
+
wrapSerializers: false,
|
|
122
116
|
})
|
|
123
117
|
}
|
|
124
118
|
|
|
125
119
|
|
|
126
|
-
get middlewareLogger() {
|
|
127
|
-
return pino({
|
|
128
|
-
level: process.env.HTTP_LOG_LEVEL || 'info',
|
|
129
|
-
transport: {
|
|
130
|
-
level: process.env.HTTP_LOG_LEVEL || 'info',
|
|
131
|
-
target: 'pino-pretty',
|
|
132
|
-
options: {
|
|
133
|
-
colorize: true,
|
|
134
|
-
ignore: 'pid,hostname',
|
|
135
|
-
translateTime: 'SYS:standard',
|
|
136
|
-
messageFormat: '{msg}',
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
120
|
get staticFilesDirectory() {
|
|
144
121
|
return `${process.cwd()}/static`;
|
|
145
122
|
}
|
|
@@ -158,6 +135,53 @@ class VidaServer {
|
|
|
158
135
|
use() { this.#expressServer.use(...arguments); }
|
|
159
136
|
|
|
160
137
|
|
|
138
|
+
/***********************************************************************************************
|
|
139
|
+
* LOGGING
|
|
140
|
+
***********************************************************************************************/
|
|
141
|
+
requestLogDetails(req, res, err) {
|
|
142
|
+
const details = {
|
|
143
|
+
req: `${req.method} ${req.url}`,
|
|
144
|
+
res: `statusCode=${res.statusCode}, responseTime=${res.get('X-Response-Time')}`,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (res.statusCode >= 500 && res.error) {
|
|
148
|
+
details.error = this.requestLogErrorDetails(req, res, res.error);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return details;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
requestLogErrorDetails(req, res, err) {
|
|
156
|
+
if (typeof err == 'string') return {message: err};
|
|
157
|
+
return {
|
|
158
|
+
type: err.constructor.name,
|
|
159
|
+
message: err.message,
|
|
160
|
+
stack: err.stack
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
requestLogMessage(req, res) {
|
|
166
|
+
const prefix = '[VidaServer]';
|
|
167
|
+
if (req.controller) {
|
|
168
|
+
return `${prefix} ${req.controller.constructor.name}#${req.action}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return `${prefix} ${res.status >= 500 ? 'request errored' : 'request completed'}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
requestLogLevel(req, res) {
|
|
176
|
+
return 'debug';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
get middlewareLogger() {
|
|
181
|
+
return logger;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
161
185
|
/***********************************************************************************************
|
|
162
186
|
* SETTINGS
|
|
163
187
|
***********************************************************************************************/
|
|
@@ -226,8 +250,9 @@ class VidaServer {
|
|
|
226
250
|
|
|
227
251
|
requestHandler(action, controllerCls) {
|
|
228
252
|
return async function(request, response) {
|
|
229
|
-
if (process.env.NODE_ENV != 'test') this.logger.info(`${controllerCls.name}#${action}`);
|
|
230
253
|
const controllerInstance = this.buildController(controllerCls, request, response);
|
|
254
|
+
request.controller = controllerInstance;
|
|
255
|
+
request.action = action;
|
|
231
256
|
await controllerInstance.performRequest(action);
|
|
232
257
|
}.bind(this);
|
|
233
258
|
}
|
|
@@ -28,6 +28,7 @@ class VidaServerController {
|
|
|
28
28
|
this.#response = response;
|
|
29
29
|
|
|
30
30
|
this.#applyCallbacks();
|
|
31
|
+
this.#setupStatusRenderers();
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
|
|
@@ -121,21 +122,26 @@ class VidaServerController {
|
|
|
121
122
|
|
|
122
123
|
async #handleError(err) {
|
|
123
124
|
if (err instanceof AuthorizationError) {
|
|
124
|
-
await this.
|
|
125
|
+
await this.renderUnauthorized(err.message);
|
|
125
126
|
|
|
126
127
|
} else if (err instanceof ForbiddenError) {
|
|
127
|
-
await this.
|
|
128
|
+
await this.renderForbidden(err.message);
|
|
128
129
|
|
|
129
130
|
} else if (err instanceof NotFoundError) {
|
|
130
|
-
await this.
|
|
131
|
+
await this.renderNotFound(err.message);
|
|
131
132
|
|
|
132
133
|
} else if (err instanceof ValidationError) {
|
|
133
134
|
await this.renderErrors(err.message, err.fields);
|
|
134
135
|
|
|
135
136
|
} else {
|
|
136
|
-
this.
|
|
137
|
-
|
|
138
|
-
if (
|
|
137
|
+
this._response.error = err;
|
|
138
|
+
let message;
|
|
139
|
+
if (typeof err == 'string') {
|
|
140
|
+
message = err;
|
|
141
|
+
} else {
|
|
142
|
+
message = err.message;
|
|
143
|
+
}
|
|
144
|
+
await this.renderServerError(message);
|
|
139
145
|
}
|
|
140
146
|
}
|
|
141
147
|
|
|
@@ -148,7 +154,11 @@ class VidaServerController {
|
|
|
148
154
|
this._response.send(body);
|
|
149
155
|
} else {
|
|
150
156
|
body = await this.processJSONBody(body, options);
|
|
151
|
-
|
|
157
|
+
|
|
158
|
+
const errors = body.errors || null;
|
|
159
|
+
delete body.errors;
|
|
160
|
+
|
|
161
|
+
body = this.formatJSONBody(body, errors, options);
|
|
152
162
|
this._response.json(body);
|
|
153
163
|
}
|
|
154
164
|
this.#rendered = true;
|
|
@@ -177,27 +187,21 @@ class VidaServerController {
|
|
|
177
187
|
}
|
|
178
188
|
|
|
179
189
|
|
|
180
|
-
async
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
async renderUnauthorizedResponse(message=null) {
|
|
187
|
-
this.statusCode = 401;
|
|
188
|
-
await this.render({ message });
|
|
190
|
+
async renderServerError(message=null) {
|
|
191
|
+
if (process.env.NODE_ENV != 'development') {
|
|
192
|
+
message = null;
|
|
193
|
+
}
|
|
194
|
+
return await this.#renderError(500, message);
|
|
189
195
|
}
|
|
190
196
|
|
|
191
197
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
await this.render({ message });
|
|
195
|
-
}
|
|
198
|
+
formatJSONBody(body, errors, options) {
|
|
199
|
+
if (options.standardize === false) return body;
|
|
196
200
|
|
|
201
|
+
const response = {data: body, status: this.statusText};
|
|
202
|
+
if (errors) response.errors = errors;
|
|
197
203
|
|
|
198
|
-
|
|
199
|
-
if (options.standardize === false) return body;
|
|
200
|
-
return {data: body, status: this.statusText};
|
|
204
|
+
return response;
|
|
201
205
|
}
|
|
202
206
|
|
|
203
207
|
|
|
@@ -231,26 +235,118 @@ class VidaServerController {
|
|
|
231
235
|
switch(this.statusCode) {
|
|
232
236
|
case 200:
|
|
233
237
|
return 'ok';
|
|
238
|
+
case 201:
|
|
239
|
+
return 'created';
|
|
240
|
+
case 202:
|
|
241
|
+
return 'accepted';
|
|
242
|
+
case 204:
|
|
243
|
+
return 'no content';
|
|
244
|
+
case 301:
|
|
245
|
+
return 'moved permanently';
|
|
246
|
+
case 302:
|
|
247
|
+
return 'found';
|
|
248
|
+
case 304:
|
|
249
|
+
return 'not modified';
|
|
250
|
+
case 307:
|
|
251
|
+
return 'temporary redirect';
|
|
252
|
+
case 308:
|
|
253
|
+
return 'permanent redirect';
|
|
234
254
|
case 400:
|
|
235
255
|
return 'bad request'
|
|
236
256
|
case 401:
|
|
237
257
|
return 'unauthorized';
|
|
258
|
+
case 402:
|
|
259
|
+
return 'payment required';
|
|
238
260
|
case 403:
|
|
239
261
|
return 'forbidden';
|
|
240
262
|
case 404:
|
|
241
263
|
return 'not found';
|
|
264
|
+
case 405:
|
|
265
|
+
return 'method not allowed';
|
|
266
|
+
case 408:
|
|
267
|
+
return 'request timeout';
|
|
268
|
+
case 409:
|
|
269
|
+
return 'conflict';
|
|
270
|
+
case 410:
|
|
271
|
+
return 'gone';
|
|
272
|
+
case 413:
|
|
273
|
+
return 'content too large';
|
|
274
|
+
case 415:
|
|
275
|
+
return 'unsupported media type';
|
|
276
|
+
case 422:
|
|
277
|
+
return 'unprocessable content';
|
|
278
|
+
case 423:
|
|
279
|
+
return 'locked';
|
|
280
|
+
case 429:
|
|
281
|
+
return 'too many requests';
|
|
242
282
|
case 500:
|
|
243
283
|
return 'server error';
|
|
244
284
|
}
|
|
245
285
|
}
|
|
246
286
|
|
|
247
287
|
|
|
288
|
+
#setupStatusRenderers() {
|
|
289
|
+
const errorStatuses = {
|
|
290
|
+
Unauthorized: 401,
|
|
291
|
+
PaymentRequired: 402,
|
|
292
|
+
Forbidden: 403,
|
|
293
|
+
NotFound: 404,
|
|
294
|
+
MethodNotAllowed: 405,
|
|
295
|
+
RequestTimeout: 408,
|
|
296
|
+
Conflict: 409,
|
|
297
|
+
Gone: 410,
|
|
298
|
+
ContentTooLarge: 413,
|
|
299
|
+
UnsupportedMediaType: 415,
|
|
300
|
+
UnprocessableContent: 422,
|
|
301
|
+
Locked: 423,
|
|
302
|
+
TooManyRequests: 429,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const successStatuses = {
|
|
306
|
+
CreationSuccessful: 201,
|
|
307
|
+
Accepted: 202,
|
|
308
|
+
NoConent: 204,
|
|
309
|
+
MovedPermanently: 301,
|
|
310
|
+
Found: 302,
|
|
311
|
+
NotModified: 304,
|
|
312
|
+
TemporaryRedirect: 307,
|
|
313
|
+
PermanentRedirect: 308,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (const [method, statusCode] of Object.entries(errorStatuses)) {
|
|
317
|
+
this[`render${method}`] = async function(message=null) {
|
|
318
|
+
return await this.#renderError(statusCode, message);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (const [method, statusCode] of Object.entries(successStatuses)) {
|
|
323
|
+
this[`render${method}`] = async function(message=null) {
|
|
324
|
+
return await this.#renderSuccess(statusCode, message);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
async #renderError(statusCode, message) {
|
|
331
|
+
this.statusCode = statusCode;
|
|
332
|
+
const body = {errors: {message: message || null}};
|
|
333
|
+
return await this.render(body);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
async #renderSuccess(statusCode, body) {
|
|
338
|
+
this.statusCode = statusCode;
|
|
339
|
+
return await this.render(body);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
|
|
248
343
|
/***********************************************************************************************
|
|
249
344
|
* HELPERS
|
|
250
345
|
***********************************************************************************************/
|
|
251
346
|
static autoLoadHelpers() {
|
|
252
|
-
const { InstanceMethods, Accessors } = this._autoLoadModule(this.autoLoadHelperPath);
|
|
347
|
+
const { StaticMethods, InstanceMethods, Accessors } = this._autoLoadModule(this.autoLoadHelperPath);
|
|
253
348
|
|
|
349
|
+
if (StaticMethods) Object.assign(this, StaticMethods);
|
|
254
350
|
if (InstanceMethods) Object.assign(this.prototype, InstanceMethods);
|
|
255
351
|
|
|
256
352
|
if (Accessors) {
|
|
@@ -363,10 +459,10 @@ class VidaServerController {
|
|
|
363
459
|
/***********************************************************************************************
|
|
364
460
|
* VALIDATIONS
|
|
365
461
|
***********************************************************************************************/
|
|
366
|
-
validateParameters(validations) {
|
|
462
|
+
async validateParameters(validations) {
|
|
367
463
|
const errors = {};
|
|
368
464
|
for (const [parameterName, parameterValidations] of Object.entries(validations)) {
|
|
369
|
-
const parameterErrors = this.validateParameter(this.params[parameterName], parameterValidations);
|
|
465
|
+
const parameterErrors = await this.validateParameter(this.params[parameterName], parameterValidations);
|
|
370
466
|
if (parameterErrors.length) errors[parameterName] = parameterErrors;
|
|
371
467
|
}
|
|
372
468
|
|
|
@@ -374,7 +470,7 @@ class VidaServerController {
|
|
|
374
470
|
}
|
|
375
471
|
|
|
376
472
|
|
|
377
|
-
validateParameter(value, parameterValidations) {
|
|
473
|
+
async validateParameter(value, parameterValidations) {
|
|
378
474
|
const errors = [];
|
|
379
475
|
|
|
380
476
|
if (parameterValidations?.optional && value === undefined) return errors;
|
|
@@ -382,7 +478,7 @@ class VidaServerController {
|
|
|
382
478
|
for (const [validationType, validationOptions] of Object.entries(parameterValidations)) {
|
|
383
479
|
const validationMethod = `validate${camelize(validationType)}`;
|
|
384
480
|
if (!this[validationMethod]) continue;
|
|
385
|
-
const error = this[validationMethod](value, validationOptions);
|
|
481
|
+
const error = await this[validationMethod](value, validationOptions);
|
|
386
482
|
if (error) errors.push(error);
|
|
387
483
|
}
|
|
388
484
|
|
|
@@ -435,8 +531,8 @@ class VidaServerController {
|
|
|
435
531
|
}
|
|
436
532
|
|
|
437
533
|
|
|
438
|
-
validateFunction(value, fnc) {
|
|
439
|
-
const error = fnc(value);
|
|
534
|
+
async validateFunction(value, fnc) {
|
|
535
|
+
const error = await fnc.call(this, value);
|
|
440
536
|
if (error) return error;
|
|
441
537
|
}
|
|
442
538
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vida-global/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Core libraries for supporting Vida development",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "ISC",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"scripts": {
|
|
9
|
-
"test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
9
|
+
"test": "LOG_LEVEL=test NODE_NO_WARNINGS=1 node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
10
10
|
"release": "node node_modules/@vida-global/release/scripts/release.js",
|
|
11
11
|
"develop": "node node_modules/@vida-global/release/scripts/release.js develop"
|
|
12
12
|
},
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"@vida-global/release": "^1.0.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"jest": "^
|
|
36
|
+
"jest": "^29.0.0",
|
|
37
37
|
"jest-express": "^1.12.0",
|
|
38
38
|
"jest-extended": "^7.0.0",
|
|
39
39
|
"@vida-global/test-helpers": "^1.0.1"
|
|
@@ -8,7 +8,7 @@ const { VidaServer } = require('../../lib/server');
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
jest.mock('express', () => {
|
|
11
|
-
const
|
|
11
|
+
const express = require('jest-express');
|
|
12
12
|
const { Response } = require('jest-express/lib/response');
|
|
13
13
|
|
|
14
14
|
// missing functions in mock express
|
|
@@ -8,7 +8,7 @@ const TestHelpers = require('@vida-global/test-helpers');
|
|
|
8
8
|
const { VidaServerController } = require('../../lib/server');
|
|
9
9
|
|
|
10
10
|
describe('VidaServerController', () => {
|
|
11
|
-
describe('
|
|
11
|
+
describe('.actionNames', () => {
|
|
12
12
|
it ('returns actions based on method names', () => {
|
|
13
13
|
const names = FooController.actionNames;
|
|
14
14
|
const expected = ['postIndex',
|
|
@@ -23,7 +23,7 @@ describe('VidaServerController', () => {
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
describe('
|
|
26
|
+
describe('.autoLoadHelpers', () => {
|
|
27
27
|
class AutoLoadTestController extends VidaServerController {
|
|
28
28
|
static get autoLoadHelperPath() {
|
|
29
29
|
return `${process.cwd()}/test/server/helpers/autoload/${this.name}.js`;
|
|
@@ -60,7 +60,7 @@ describe('VidaServerController', () => {
|
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
describe('
|
|
63
|
+
describe('._constructAction', () => {
|
|
64
64
|
class UsersController extends VidaServerController {
|
|
65
65
|
static get routes() {
|
|
66
66
|
return {getBazBan: '/baz/:id/ban'};
|
|
@@ -123,7 +123,7 @@ describe('VidaServerController', () => {
|
|
|
123
123
|
});
|
|
124
124
|
|
|
125
125
|
|
|
126
|
-
describe('
|
|
126
|
+
describe('.validateParameters', () => {
|
|
127
127
|
const paramName1 = TestHelpers.Faker.Text.randomString();
|
|
128
128
|
const paramName2 = TestHelpers.Faker.Text.randomString();
|
|
129
129
|
const value1 = TestHelpers.Faker.Text.randomString();
|
|
@@ -155,8 +155,8 @@ describe('VidaServerController', () => {
|
|
|
155
155
|
[paramName2]: {[validationType1]: options2}
|
|
156
156
|
};
|
|
157
157
|
|
|
158
|
-
it ('runs validation checkers based on inputs', () => {
|
|
159
|
-
controller.validateParameters(validations);
|
|
158
|
+
it ('runs validation checkers based on inputs', async () => {
|
|
159
|
+
await controller.validateParameters(validations);
|
|
160
160
|
expect(validator1).toHaveBeenCalledTimes(2);
|
|
161
161
|
expect(validator1).toHaveBeenCalledWith(value1, options1);
|
|
162
162
|
expect(validator1).toHaveBeenCalledWith(value2, options2);
|
|
@@ -165,59 +165,51 @@ describe('VidaServerController', () => {
|
|
|
165
165
|
expect(validator2).toHaveBeenCalledWith(value1, options2);
|
|
166
166
|
});
|
|
167
167
|
|
|
168
|
-
it ('throws a `ValidationError` when there\'s an error', () => {
|
|
168
|
+
it ('throws a `ValidationError` when there\'s an error', async () => {
|
|
169
169
|
validator1.mockImplementation(() => error1);
|
|
170
|
-
expect(()
|
|
171
|
-
controller.validateParameters(validations);
|
|
172
|
-
}).toThrow(Errors.ValidationError);
|
|
170
|
+
await expect(controller.validateParameters(validations)).rejects.toThrow();
|
|
173
171
|
});
|
|
174
172
|
|
|
175
|
-
it ('throws a `ValidationError` when there are multiple errors', () => {
|
|
173
|
+
it ('throws a `ValidationError` when there are multiple errors', async () => {
|
|
176
174
|
validator1.mockImplementation(() => error1);
|
|
177
175
|
validator2.mockImplementation(() => error2);
|
|
178
176
|
|
|
179
|
-
expect(()
|
|
180
|
-
controller.validateParameters(validations);
|
|
181
|
-
}).toThrow(Errors.ValidationError);
|
|
177
|
+
await expect(controller.validateParameters(validations)).rejects.toThrow(Errors.ValidationError);
|
|
182
178
|
});
|
|
183
179
|
|
|
184
|
-
it ('sets nothing when there is no error', () => {
|
|
185
|
-
controller.validateParameters(validations);
|
|
180
|
+
it ('sets nothing when there is no error', async () => {
|
|
181
|
+
await controller.validateParameters(validations);
|
|
186
182
|
expect(controller.render).toHaveBeenCalledTimes(0);
|
|
187
183
|
expect(response.statusCode).toEqual(200);
|
|
188
184
|
});
|
|
189
185
|
|
|
190
|
-
it ('ignores details where there is no validator', () => {
|
|
186
|
+
it ('ignores details where there is no validator', async () => {
|
|
191
187
|
const validations = {
|
|
192
188
|
[paramName2]: {[validationType1]: options2, 'foo': true}
|
|
193
189
|
};
|
|
194
|
-
controller.validateParameters(validations);
|
|
190
|
+
await controller.validateParameters(validations);
|
|
195
191
|
|
|
196
192
|
expect(validator1).toHaveBeenCalledTimes(1);
|
|
197
193
|
expect(validator1).toHaveBeenCalledWith(value2, options2);
|
|
198
194
|
});
|
|
199
195
|
|
|
200
|
-
it ('does not error when optional parameters are not included', () => {
|
|
196
|
+
it ('does not error when optional parameters are not included', async () => {
|
|
201
197
|
const validations = {foo: {isString: true, optional: true}};
|
|
202
|
-
expect(()
|
|
203
|
-
controller.validateParameters(validations);
|
|
204
|
-
}).not.toThrow();
|
|
198
|
+
expect(controller.validateParameters(validations)).resolves.not.toThrow();
|
|
205
199
|
});
|
|
206
200
|
|
|
207
|
-
it ('does error when optional parameters are invalid', () => {
|
|
201
|
+
it ('does error when optional parameters are invalid', async () => {
|
|
208
202
|
const validations = {foo: {isString: {length: {gte: 2}}, optional: true}};
|
|
209
203
|
params.foo = '';
|
|
210
204
|
|
|
211
|
-
expect(()
|
|
212
|
-
controller.validateParameters(validations);
|
|
213
|
-
}).toThrow(Errors.ValidationError);
|
|
205
|
+
await expect(controller.validateParameters(validations)).rejects.toThrow(Errors.ValidationError);
|
|
214
206
|
|
|
215
207
|
delete params.foo;
|
|
216
208
|
});
|
|
217
209
|
});
|
|
218
210
|
|
|
219
211
|
|
|
220
|
-
describe('
|
|
212
|
+
describe('.validatePresence', () => {
|
|
221
213
|
const controller = new FooController({}, {});
|
|
222
214
|
it ('returns undefined when there is a value', () => {
|
|
223
215
|
const result = controller.validatePresence(TestHelpers.Faker.Text.randomString());
|
|
@@ -231,7 +223,7 @@ describe('VidaServerController', () => {
|
|
|
231
223
|
});
|
|
232
224
|
|
|
233
225
|
|
|
234
|
-
describe('
|
|
226
|
+
describe('.validateIsInteger', () => {
|
|
235
227
|
const controller = new FooController({}, {});
|
|
236
228
|
it ('returns undefined when there is an integer', () => {
|
|
237
229
|
const val = Math.ceil(Math.random() * 100);
|
|
@@ -316,7 +308,7 @@ describe('VidaServerController', () => {
|
|
|
316
308
|
});
|
|
317
309
|
|
|
318
310
|
|
|
319
|
-
describe('
|
|
311
|
+
describe('.validateIsString', () => {
|
|
320
312
|
const controller = new FooController({}, {});
|
|
321
313
|
it ('returns undefined when there is an string', () => {
|
|
322
314
|
const result = controller.validateIsString('')
|
|
@@ -396,7 +388,7 @@ describe('VidaServerController', () => {
|
|
|
396
388
|
});
|
|
397
389
|
|
|
398
390
|
|
|
399
|
-
describe('
|
|
391
|
+
describe('.validateIsDateTime', () => {
|
|
400
392
|
const controller = new FooController({}, {});
|
|
401
393
|
it ('returns undefined when passed a string of format YYYY-MM-DD', () => {
|
|
402
394
|
const result = controller.validateIsDateTime('2025-12-17');
|
|
@@ -435,7 +427,7 @@ describe('VidaServerController', () => {
|
|
|
435
427
|
});
|
|
436
428
|
|
|
437
429
|
|
|
438
|
-
describe('
|
|
430
|
+
describe('.validateIsEnum', () => {
|
|
439
431
|
const controller = new FooController({}, {});
|
|
440
432
|
const enums = [Math.random(), Math.random(), Math.random()];
|
|
441
433
|
const error = TestHelpers.Faker.Text.randomString();
|
|
@@ -452,30 +444,30 @@ describe('VidaServerController', () => {
|
|
|
452
444
|
});
|
|
453
445
|
|
|
454
446
|
|
|
455
|
-
describe('
|
|
447
|
+
describe('.validateFunction', () => {
|
|
456
448
|
const controller = new FooController({}, {});
|
|
457
449
|
const correctValue = Math.random();
|
|
458
450
|
const error = TestHelpers.Faker.Text.randomString();
|
|
459
451
|
const validator = (value) => value == correctValue ? undefined : error;
|
|
460
452
|
|
|
461
|
-
it ('returns undefiend when passed a valid value', () => {
|
|
462
|
-
const result = controller.validateFunction(correctValue, validator);
|
|
453
|
+
it ('returns undefiend when passed a valid value', async () => {
|
|
454
|
+
const result = await controller.validateFunction(correctValue, validator);
|
|
463
455
|
expect(result).toBe(undefined);
|
|
464
456
|
});
|
|
465
457
|
|
|
466
|
-
it ('returns an error when the function returns an error', () => {
|
|
467
|
-
const result = controller.validateFunction(Math.random(), validator);
|
|
458
|
+
it ('returns an error when the function returns an error', async () => {
|
|
459
|
+
const result = await controller.validateFunction(Math.random(), validator);
|
|
468
460
|
expect(result).toBe(error);
|
|
469
461
|
});
|
|
470
462
|
});
|
|
471
463
|
|
|
472
464
|
|
|
473
|
-
describe('
|
|
465
|
+
describe('#performRequest', () => {
|
|
474
466
|
const errorHandlers = [
|
|
475
|
-
['
|
|
476
|
-
['
|
|
477
|
-
['
|
|
478
|
-
['renderErrors',
|
|
467
|
+
['renderUnauthorized', 'AuthorizationError'],
|
|
468
|
+
['renderForbidden', 'ForbiddenError'],
|
|
469
|
+
['renderNotFound', 'NotFoundError'],
|
|
470
|
+
['renderErrors', 'ValidationError'],
|
|
479
471
|
]
|
|
480
472
|
|
|
481
473
|
it.each(errorHandlers)('calls %s when %s is thrown', async (handlerName, errorClsName) => {
|
|
@@ -505,7 +497,7 @@ describe('VidaServerController', () => {
|
|
|
505
497
|
await controller.performRequest('testAction');
|
|
506
498
|
|
|
507
499
|
expect(controller.statusCode).toEqual(500);
|
|
508
|
-
expect(controller.render).toHaveBeenCalledWith({
|
|
500
|
+
expect(controller.render).toHaveBeenCalledWith({errors: {message: null}});
|
|
509
501
|
});
|
|
510
502
|
});
|
|
511
503
|
|
|
@@ -519,8 +511,7 @@ describe('VidaServerController', () => {
|
|
|
519
511
|
controller.render = jest.fn();
|
|
520
512
|
});
|
|
521
513
|
|
|
522
|
-
|
|
523
|
-
describe('VidaServerController#renderErrors', () => {
|
|
514
|
+
describe('#renderErrors', () => {
|
|
524
515
|
it ('returns a 400 response', async () => {
|
|
525
516
|
await controller.renderErrors();
|
|
526
517
|
expect(response.statusCode).toBe(400);
|
|
@@ -576,66 +567,68 @@ describe('VidaServerController', () => {
|
|
|
576
567
|
});
|
|
577
568
|
});
|
|
578
569
|
|
|
579
|
-
|
|
580
|
-
describe('VidaServerController#renderUnauthorizedResponse', () => {
|
|
581
|
-
it ('returns a 401 response', async () => {
|
|
582
|
-
await controller.renderUnauthorizedResponse();
|
|
583
|
-
expect(response.statusCode).toBe(401);
|
|
584
|
-
})
|
|
585
|
-
|
|
586
|
-
it ('includes a blank message by default', async () => {
|
|
587
|
-
await controller.renderUnauthorizedResponse();
|
|
588
|
-
expect(controller.render).toHaveBeenCalledTimes(1);
|
|
589
|
-
expect(controller.render).toHaveBeenCalledWith({message: null});
|
|
590
|
-
})
|
|
591
570
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
571
|
+
const successHandlers = [
|
|
572
|
+
['renderCreationSuccessful', 201],
|
|
573
|
+
['renderAccepted', 202],
|
|
574
|
+
['renderNoConent', 204],
|
|
575
|
+
['renderMovedPermanently', 301],
|
|
576
|
+
['renderFound', 302],
|
|
577
|
+
['renderNotModified', 304],
|
|
578
|
+
['renderTemporaryRedirect', 307],
|
|
579
|
+
['renderPermanentRedirect', 308],
|
|
580
|
+
]
|
|
581
|
+
describe.each(successHandlers)('#%s', (handlerName, statusCode) => {
|
|
582
|
+
it (`returns a ${statusCode} response`, async () => {
|
|
583
|
+
await (controller[handlerName]());
|
|
584
|
+
expect(response.statusCode).toBe(statusCode);
|
|
605
585
|
})
|
|
606
586
|
|
|
607
|
-
it ('includes
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
587
|
+
it ('includes the provided body', async () => {
|
|
588
|
+
const key = TestHelpers.Faker.Text.randomString();
|
|
589
|
+
const value = TestHelpers.Faker.Text.randomString();
|
|
590
|
+
const body = {[key]: value};
|
|
591
|
+
await (controller[handlerName](body));
|
|
612
592
|
|
|
613
|
-
it ('includes a provided message', async () => {
|
|
614
|
-
const msg = TestHelpers.Faker.Text.randomString();
|
|
615
|
-
await controller.renderForbiddenResponse(msg);
|
|
616
593
|
expect(controller.render).toHaveBeenCalledTimes(1);
|
|
617
|
-
expect(controller.render).toHaveBeenCalledWith(
|
|
594
|
+
expect(controller.render).toHaveBeenCalledWith(body);
|
|
618
595
|
});
|
|
619
596
|
});
|
|
620
597
|
|
|
598
|
+
|
|
599
|
+
const errorHandlers = [
|
|
600
|
+
['renderUnauthorized', 401],
|
|
601
|
+
['renderPaymentRequired', 402],
|
|
602
|
+
['renderForbidden', 403],
|
|
603
|
+
['renderNotFound', 404],
|
|
604
|
+
['renderMethodNotAllowed', 405],
|
|
605
|
+
['renderRequestTimeout', 408],
|
|
606
|
+
['renderConflict', 409],
|
|
607
|
+
['renderGone', 410],
|
|
608
|
+
['renderContentTooLarge', 413],
|
|
609
|
+
['renderUnsupportedMediaType', 415],
|
|
610
|
+
['renderUnprocessableContent', 422],
|
|
611
|
+
['renderLocked', 423],
|
|
612
|
+
['renderTooManyRequests', 429],
|
|
613
|
+
]
|
|
621
614
|
|
|
622
|
-
describe('
|
|
623
|
-
it (
|
|
624
|
-
await controller
|
|
625
|
-
expect(response.statusCode).toBe(
|
|
615
|
+
describe.each(errorHandlers)('#%s', (handlerName, statusCode) => {
|
|
616
|
+
it (`returns a ${statusCode} response`, async () => {
|
|
617
|
+
await (controller[handlerName]());
|
|
618
|
+
expect(response.statusCode).toBe(statusCode);
|
|
626
619
|
})
|
|
627
620
|
|
|
628
621
|
it ('includes a blank message by default', async () => {
|
|
629
|
-
await controller
|
|
622
|
+
await controller[handlerName]();
|
|
630
623
|
expect(controller.render).toHaveBeenCalledTimes(1);
|
|
631
|
-
expect(controller.render).toHaveBeenCalledWith({message: null});
|
|
624
|
+
expect(controller.render).toHaveBeenCalledWith({errors: {message: null}});
|
|
632
625
|
})
|
|
633
626
|
|
|
634
627
|
it ('includes a provided message', async () => {
|
|
635
628
|
const msg = TestHelpers.Faker.Text.randomString();
|
|
636
|
-
await controller
|
|
629
|
+
await controller[handlerName](msg);
|
|
637
630
|
expect(controller.render).toHaveBeenCalledTimes(1);
|
|
638
|
-
expect(controller.render).toHaveBeenCalledWith({message: msg});
|
|
631
|
+
expect(controller.render).toHaveBeenCalledWith({errors: {message: msg}});
|
|
639
632
|
});
|
|
640
633
|
});
|
|
641
634
|
});
|
|
@@ -653,7 +646,7 @@ describe('VidaServerController', () => {
|
|
|
653
646
|
controller = new FooController(request, response);
|
|
654
647
|
});
|
|
655
648
|
|
|
656
|
-
describe('
|
|
649
|
+
describe('#requestHeaders', () => {
|
|
657
650
|
it ('returns a copy of the request headers', () => {
|
|
658
651
|
expect(controller.requestHeaders).not.toBe(request.headers);
|
|
659
652
|
expect(JSON.stringify(controller.requestHeaders)).toEqual(JSON.stringify(request.headers));
|
|
@@ -661,28 +654,28 @@ describe('VidaServerController', () => {
|
|
|
661
654
|
});
|
|
662
655
|
|
|
663
656
|
|
|
664
|
-
describe('
|
|
657
|
+
describe('#responseHeaders', () => {
|
|
665
658
|
it ('returns the request headers', () => {
|
|
666
659
|
expect(controller.responseHeaders).toBe(response.headers);
|
|
667
660
|
});
|
|
668
661
|
});
|
|
669
662
|
|
|
670
663
|
|
|
671
|
-
describe('
|
|
664
|
+
describe('#contentType', () => {
|
|
672
665
|
it ('returns the content-type request header', () => {
|
|
673
666
|
expect(controller.contentType).toEqual(request.headers['content-type']);
|
|
674
667
|
});
|
|
675
668
|
});
|
|
676
669
|
|
|
677
670
|
|
|
678
|
-
describe('
|
|
671
|
+
describe('#logger', () => {
|
|
679
672
|
it ('uses the standard logger', () => {
|
|
680
673
|
expect(controller.logger).toBe(logger);
|
|
681
674
|
});
|
|
682
675
|
});
|
|
683
676
|
|
|
684
677
|
|
|
685
|
-
describe('
|
|
678
|
+
describe('#rendered', () => {
|
|
686
679
|
it ('returns false if nothing has rendered', () => {
|
|
687
680
|
expect(controller.rendered).toBeFalsy();
|
|
688
681
|
});
|
|
@@ -701,7 +694,7 @@ describe('VidaServerController', () => {
|
|
|
701
694
|
});
|
|
702
695
|
|
|
703
696
|
|
|
704
|
-
describe('
|
|
697
|
+
describe('#statusCode', () => {
|
|
705
698
|
it ('returns the response status code', () => {
|
|
706
699
|
expect(controller.statusCode).toEqual(response.statusCode);
|
|
707
700
|
});
|
|
@@ -715,7 +708,7 @@ describe('VidaServerController', () => {
|
|
|
715
708
|
});
|
|
716
709
|
|
|
717
710
|
|
|
718
|
-
describe('
|
|
711
|
+
describe('#setupCallbacks', () => {
|
|
719
712
|
it ('is only called once', () => {
|
|
720
713
|
class CallbacksTestController extends VidaServerController {}
|
|
721
714
|
CallbacksTestController.prototype.setupCallbacks = jest.fn();
|