@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.
@@ -22,7 +22,7 @@ jobs:
22
22
  - name: Setup Node
23
23
  uses: actions/setup-node@v4
24
24
  with:
25
- node-version: '20'
25
+ node-version: '24'
26
26
  registry-url: 'https://registry.npmjs.org'
27
27
 
28
28
  - name: Update npm
@@ -16,7 +16,7 @@ jobs:
16
16
  - name: Setup Node
17
17
  uses: actions/setup-node@v4
18
18
  with:
19
- node-version: '20'
19
+ node-version: '24'
20
20
  registry-url: 'https://registry.npmjs.org'
21
21
 
22
22
  - name: Install dependencies
@@ -1,7 +1,10 @@
1
1
  const pino = require('pino');
2
2
 
3
3
  const config = {
4
- level: process.env.LOG_LEVEL || 'info'
4
+ level: process.env.LOG_LEVEL || 'info',
5
+ customLevels: {
6
+ test: 1000
7
+ }
5
8
  };
6
9
 
7
10
  if (!process.env.ENV_VERCEL) {
@@ -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.renderUnauthorizedResponse("Nope")
162
+ await this.renderUnauthorized("Nope")
138
163
  // renders {data: {message: "Nope"}, status: "unauthorized"}
139
164
  ```
140
165
  HTTP 403
141
166
  ```
142
- await this.renderForbiddenResponse("You shall not pass")
167
+ await this.renderForbidden("You shall not pass")
143
168
  // renders {data: {message: "You shall not pass"}, status: "forbidden"}
144
169
  ```
145
- HTTP 404
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
 
@@ -106,40 +106,17 @@ class VidaServer {
106
106
 
107
107
  get loggingMiddleware() {
108
108
  return httpLogger({
109
- logger: this.middlewareLogger,
110
- customLogLevel: (res, err) => {
111
- if (res.statusCode >= 500) return 'error';
112
- if (res.statusCode >= 400) return 'warn';
113
- return 'info';
114
- },
115
- serializers: {
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.renderUnauthorizedResponse(err.message);
125
+ await this.renderUnauthorized(err.message);
125
126
 
126
127
  } else if (err instanceof ForbiddenError) {
127
- await this.renderForbiddenResponse(err.message);
128
+ await this.renderForbidden(err.message);
128
129
 
129
130
  } else if (err instanceof NotFoundError) {
130
- await this.renderNotFoundResponse(err.message);
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.statusCode = 500;
137
- await this.render({error: err.message});
138
- if (process.env.NODE_ENV == 'development') console.log(err);
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
- body = this.formatJSONBody(body, options);
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 renderNotFoundResponse(message=null) {
181
- this.statusCode = 404;
182
- await this.render({ message });
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
- async renderForbiddenResponse(message=null) {
193
- this.statusCode = 403;
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
- formatJSONBody(body, options) {
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.2",
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": "^30.0.0",
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 express = require('jest-express');
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('VidaServerController.actionNames', () => {
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('VidaServerController.autoLoadHelpers', () => {
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('VidaServerController._constructAction', () => {
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('VidaServerController.validateParameters', () => {
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('VidaServerController.validatePresence', () => {
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('VidaServerController.validateIsInteger', () => {
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('VidaServerController.validateIsString', () => {
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('VidaServerController.validateIsDateTime', () => {
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('VidaServerController.validateIsEnum', () => {
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('VidaServerController.validateFunction', () => {
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('VidaServerController#performRequest', () => {
465
+ describe('#performRequest', () => {
474
466
  const errorHandlers = [
475
- ['renderUnauthorizedResponse', 'AuthorizationError'],
476
- ['renderForbiddenResponse', 'ForbiddenError'],
477
- ['renderNotFoundResponse', 'NotFoundError'],
478
- ['renderErrors', 'ValidationError'],
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({error: message});
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
- it ('includes a provided message', async () => {
593
- const msg = TestHelpers.Faker.Text.randomString();
594
- await controller.renderUnauthorizedResponse(msg);
595
- expect(controller.render).toHaveBeenCalledTimes(1);
596
- expect(controller.render).toHaveBeenCalledWith({message: msg});
597
- });
598
- });
599
-
600
-
601
- describe('VidaServerController#renderForbiddenResponse', () => {
602
- it ('returns a 403 response', async () => {
603
- await controller.renderForbiddenResponse();
604
- expect(response.statusCode).toBe(403);
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 a blank message by default', async () => {
608
- await controller.renderForbiddenResponse();
609
- expect(controller.render).toHaveBeenCalledTimes(1);
610
- expect(controller.render).toHaveBeenCalledWith({message: null});
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({message: msg});
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('VidaServerController#renderNotFoundResponse', () => {
623
- it ('returns a 404 response', async () => {
624
- await controller.renderNotFoundResponse();
625
- expect(response.statusCode).toBe(404);
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.renderNotFoundResponse();
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.renderNotFoundResponse(msg);
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('VidaServerController#requestHeaders', () => {
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('VidaServerController#responseHeaders', () => {
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('VidaServerController#contentType', () => {
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('VidaServerController#logger', () => {
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('VidaServerController#rendered', () => {
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('VidaServerController#statusCode', () => {
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('ServerController#setupCallbacks', () => {
711
+ describe('#setupCallbacks', () => {
719
712
  it ('is only called once', () => {
720
713
  class CallbacksTestController extends VidaServerController {}
721
714
  CallbacksTestController.prototype.setupCallbacks = jest.fn();