@tryghost/express-test 0.3.0 → 0.5.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.
@@ -1,10 +1,29 @@
1
1
  const assert = require('assert');
2
2
  const {Request, RequestOptions} = require('./request');
3
3
  const {snapshotManager} = require('@tryghost/jest-snapshot');
4
+ const {makeMessageFromMatchMessage} = require('./utils');
5
+
6
+ /**
7
+ * @typedef {object} ExpressTestAssertion
8
+ * @prop {function} fn - Wrapped assertion with response context
9
+ * @prop {string} [message] - The message to display if the assertion fails
10
+ * @prop {string} [method] - The method to call on the assertion object
11
+ * @prop {'header' | 'status'} [type] - The type of assertion
12
+ * @prop {import('assert').AssertionError} [error] - The error to throw if the assertion fails
13
+ * @prop {any} [expected]
14
+ * @prop {any} [expectedValue]
15
+ * @prop {string} [expectedField]
16
+ * @prop {object} [properties]
17
+ */
4
18
 
5
19
  class ExpectRequest extends Request {
6
20
  constructor(...args) {
7
21
  super(...args);
22
+
23
+ /**
24
+ * @property {ExpressTestAssertion[]} assertions
25
+ */
26
+ this.assertions = [];
8
27
  }
9
28
 
10
29
  expect(callback) {
@@ -35,7 +54,8 @@ class ExpectRequest extends Request {
35
54
  expectStatus(expected) {
36
55
  const assertion = {
37
56
  fn: this._assertStatus,
38
- expected
57
+ expected,
58
+ type: 'status'
39
59
  };
40
60
 
41
61
  this._addAssertion(assertion);
@@ -47,7 +67,8 @@ class ExpectRequest extends Request {
47
67
  const assertion = {
48
68
  fn: this._assertHeader,
49
69
  expectedField: expectedField.toLowerCase(),
50
- expectedValue
70
+ expectedValue,
71
+ type: 'header'
51
72
  };
52
73
 
53
74
  this._addAssertion(assertion);
@@ -55,7 +76,7 @@ class ExpectRequest extends Request {
55
76
  return this;
56
77
  }
57
78
 
58
- matchBodySnapshot(properties) {
79
+ matchBodySnapshot(properties = {}) {
59
80
  let assertion = {
60
81
  fn: this._assertSnapshot,
61
82
  properties: properties,
@@ -67,11 +88,12 @@ class ExpectRequest extends Request {
67
88
  return this;
68
89
  }
69
90
 
70
- matchHeaderSnapshot(properties) {
91
+ matchHeaderSnapshot(properties = {}) {
71
92
  let assertion = {
72
93
  fn: this._assertSnapshot,
73
94
  properties: properties,
74
- field: 'headers'
95
+ field: 'headers',
96
+ type: 'header'
75
97
  };
76
98
 
77
99
  this._addAssertion(assertion);
@@ -86,7 +108,7 @@ class ExpectRequest extends Request {
86
108
  }
87
109
 
88
110
  try {
89
- if (this.assertions) {
111
+ if (this.assertions && this.assertions.length) {
90
112
  this._assertAll(response);
91
113
  }
92
114
  return callback(null, response);
@@ -96,13 +118,51 @@ class ExpectRequest extends Request {
96
118
  });
97
119
  }
98
120
 
121
+ /**
122
+ * @description Runs through the assertions queue and executes them
123
+ * in following order:
124
+ * 1. Assertions with no type, e.g: body snapshot match, generic assertions
125
+ * 2. Assertions of "header" type
126
+ * 3. Assertions of "status" type
127
+ *
128
+ * The order is here to assert the most informative assertions first and the
129
+ * least informative ones last. For example status code assertions, usually
130
+ * are setup first but give the least amount of information needed to
131
+ * investigate the reason of failure. Same problem with header assertions.
132
+ * By changing the default order of assertions it's expected to improve
133
+ * the developer experience creating/debugging tests.
134
+ *
135
+ * @param {Object} response - HTTP response object
136
+ */
99
137
  _assertAll(response) {
138
+ const statusCodeAssertions = [];
139
+ const headerAssertions = [];
140
+
100
141
  for (const assertion of this.assertions) {
142
+ if (!['status', 'header'].includes(assertion.type)) {
143
+ assertion.fn(response, assertion);
144
+ } else if (assertion.type === 'status') {
145
+ statusCodeAssertions.push(assertion);
146
+ } else if (assertion.type === 'header') {
147
+ headerAssertions.push(assertion);
148
+ }
149
+ }
150
+
151
+ for (const assertion of headerAssertions) {
152
+ assertion.fn(response, assertion);
153
+ }
154
+
155
+ for (const assertion of statusCodeAssertions) {
101
156
  assertion.fn(response, assertion);
102
157
  }
103
158
  }
104
159
 
160
+ /**
161
+ * Adds assertion to the queue of assertions to be performed
162
+ * @param {ExpressTestAssertion} assertion
163
+ */
105
164
  _addAssertion(assertion) {
165
+ // We create the error here so that we get a useful stack trace
106
166
  let error = new assert.AssertionError({
107
167
  message: 'Unexpected assertion error',
108
168
  expected: assertion.expected,
@@ -112,7 +172,6 @@ class ExpectRequest extends Request {
112
172
  error.contextString = this.reqOptions.toString();
113
173
  assertion.error = error;
114
174
 
115
- this.assertions = this.assertions || [];
116
175
  this.assertions.push(assertion);
117
176
  }
118
177
 
@@ -120,6 +179,11 @@ class ExpectRequest extends Request {
120
179
  const {error} = assertion;
121
180
 
122
181
  error.message = `Expected statusCode ${assertion.expected}, got statusCode ${response.statusCode} ${error.contextString}`;
182
+
183
+ if (response.body && response.body.errors && response.body.errors[0].message) {
184
+ error.message += `\n${response.body.errors[0].message}`;
185
+ }
186
+
123
187
  error.actual = response.statusCode;
124
188
 
125
189
  assert.equal(response.statusCode, assertion.expected, error);
@@ -152,13 +216,32 @@ class ExpectRequest extends Request {
152
216
  _assertSnapshot(result, assertion) {
153
217
  const {properties, field, error} = assertion;
154
218
 
155
- const match = snapshotManager.match(result[field], properties);
219
+ if (!result[field]) {
220
+ error.message = `Unable to match snapshot on undefined field ${field} ${error.contextString}`;
221
+ error.expected = field;
222
+ error.actual = 'undefined';
223
+ assert.notEqual(result[field], undefined, error);
224
+ }
225
+
226
+ const hint = `[${field}]`;
227
+ const match = snapshotManager.match(result[field], properties, hint);
228
+
229
+ Object.keys(properties).forEach((prop) => {
230
+ const errorMessage = `"response.${field}" is missing the expected property "${prop}"`;
231
+ error.message = makeMessageFromMatchMessage(match.message(), errorMessage);
232
+ error.expected = prop;
233
+ error.actual = 'undefined';
234
+ error.showDiff = false; // Disable mocha's diff output as it's already present in match.message()
235
+
236
+ assert.notEqual(result[field][prop], undefined, error);
237
+ });
156
238
 
157
239
  if (match.pass !== true) {
158
- const snapshotName = match.message().match(/Snapshot name: `(.*)`/)[1];
240
+ const errorMessage = `"response.${field}" does not match snapshot.`;
241
+ error.message = makeMessageFromMatchMessage(match.message(), errorMessage);
159
242
  error.expected = match.expected;
160
243
  error.actual = match.actual;
161
- error.message = `Expected ${field} to match in Snapshot: "${snapshotName}" ${error.contextString}`;
244
+ error.showDiff = false; // Disable mocha's diff output as it's already present in match.message()
162
245
  }
163
246
 
164
247
  assert.equal(match.pass, true, error);
package/lib/request.js CHANGED
@@ -67,7 +67,7 @@ class Request {
67
67
  const {req, res} = reqresnext(Object.assign({}, reqOptions, {app}), {app});
68
68
 
69
69
  // This is needed to make error handling work
70
- // @TODO: submit this upstream in reqresnext
70
+ // @TODO: remove once https://github.com/antongolub/reqresnext/issues/31 is solved
71
71
  req.socket.destroy = () => { };
72
72
  return {req, res};
73
73
  }
package/lib/utils.js CHANGED
@@ -3,3 +3,9 @@ module.exports.isJSON = function isJSON(mime) {
3
3
  // but not /json-seq
4
4
  return /[/+]json($|[^-\w])/i.test(mime);
5
5
  };
6
+
7
+ module.exports.makeMessageFromMatchMessage = function makeMessageFromMatchMessage(message, errorMessage) {
8
+ const messageLines = message.split('\n');
9
+ messageLines.splice(0, 1, errorMessage);
10
+ return messageLines.join('\n');
11
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryghost/express-test",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "repository": "https://github.com/TryGhost/framework/tree/main/packages/express-test",
5
5
  "author": "Ghost Foundation",
6
6
  "license": "MIT",
@@ -20,15 +20,15 @@
20
20
  },
21
21
  "devDependencies": {
22
22
  "c8": "7.11.0",
23
- "express": "^4.17.2",
24
- "express-session": "^1.17.2",
23
+ "express": "4.17.2",
24
+ "express-session": "1.17.2",
25
25
  "mocha": "9.2.0",
26
26
  "sinon": "13.0.1"
27
27
  },
28
28
  "dependencies": {
29
- "@tryghost/jest-snapshot": "^0.2.0",
29
+ "@tryghost/jest-snapshot": "^0.3.2",
30
30
  "cookiejar": "^2.1.3",
31
- "reqresnext": "^1.6.6"
31
+ "reqresnext": "^1.7.0"
32
32
  },
33
- "gitHead": "1750e2bdc5b8f659a4439490c79a8c262a74e66f"
33
+ "gitHead": "e954893434fda3f8c8de1970aaaa6f6a8b297f5d"
34
34
  }