@tryghost/express-test 0.4.0 → 0.6.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/README.md CHANGED
@@ -40,6 +40,16 @@ return await agent
40
40
  This is an initial version for review. More docs coming if it works :)
41
41
 
42
42
 
43
+ ### Assertion execution order
44
+ The order of *chained* assertion execution is NOT the order they were declared in. The framework follows the priority order of the assertion types:
45
+ 1. `expect`
46
+ 2. `expectHeader`
47
+ 3. `expectStatus`
48
+
49
+ Meaning, any `expect` declarations would be asserted first following by `expectHeader` and `expectStatus` declarations.
50
+
51
+ The custom order is here to prioritize assertions with the most context first. The feature was added based on frustrations with frameworks like [superagent](https://github.com/visionmedia/superagent), which didn't provide enough information when the status code assertions failed.
52
+
43
53
  ## Develop
44
54
 
45
55
  This is a mono repository, managed with [lerna](https://lernajs.io/).
package/index.js CHANGED
@@ -1 +1,9 @@
1
1
  module.exports = require('./lib/agent');
2
+
3
+ // NOTE: exposing jest-snapshot as a part of express-test to avoid
4
+ // version missmatching on the client side
5
+ module.exports.snapshot = {
6
+ mochaHooks: require('@tryghost/jest-snapshot').mochaHooks,
7
+ any: require('@tryghost/jest-snapshot').any,
8
+ stringMatching: require('@tryghost/jest-snapshot').stringMatching
9
+ };
package/lib/agent.js CHANGED
@@ -3,6 +3,14 @@ const ExpectRequest = require('./expect-request');
3
3
  const {RequestOptions} = require('./request');
4
4
 
5
5
  class Agent {
6
+ /**
7
+ *
8
+ * @param {Object} app instance of Express app
9
+ * @param {Object} [defaults]
10
+ * @param {string} [defaults.baseUrl]
11
+ * @param {Object} [defaults.headers]
12
+ * @param {Object} [defaults.queryParams] - custom query params to append to each request
13
+ */
6
14
  constructor(app, defaults = {}) {
7
15
  this.app = app;
8
16
  this.defaults = defaults;
@@ -15,6 +23,20 @@ class Agent {
15
23
  url = `/${this.defaults.baseUrl}/${url}`.replace(/(^|[^:])\/\/+/g, '$1/');
16
24
  }
17
25
 
26
+ if (this.defaults.queryParams) {
27
+ const searchParams = new URLSearchParams();
28
+
29
+ for (const key in this.defaults.queryParams) {
30
+ searchParams.append(key, this.defaults.queryParams[key]);
31
+ }
32
+
33
+ if (url.includes('?')) {
34
+ url = `${url}&${searchParams.toString()}`;
35
+ } else {
36
+ url = `${url}?${searchParams.toString()}`;
37
+ }
38
+ }
39
+
18
40
  return url;
19
41
  }
20
42
 
@@ -3,9 +3,27 @@ const {Request, RequestOptions} = require('./request');
3
3
  const {snapshotManager} = require('@tryghost/jest-snapshot');
4
4
  const {makeMessageFromMatchMessage} = require('./utils');
5
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
+ */
18
+
6
19
  class ExpectRequest extends Request {
7
20
  constructor(...args) {
8
21
  super(...args);
22
+
23
+ /**
24
+ * @property {ExpressTestAssertion[]} assertions
25
+ */
26
+ this.assertions = [];
9
27
  }
10
28
 
11
29
  expect(callback) {
@@ -36,7 +54,8 @@ class ExpectRequest extends Request {
36
54
  expectStatus(expected) {
37
55
  const assertion = {
38
56
  fn: this._assertStatus,
39
- expected
57
+ expected,
58
+ type: 'status'
40
59
  };
41
60
 
42
61
  this._addAssertion(assertion);
@@ -48,7 +67,8 @@ class ExpectRequest extends Request {
48
67
  const assertion = {
49
68
  fn: this._assertHeader,
50
69
  expectedField: expectedField.toLowerCase(),
51
- expectedValue
70
+ expectedValue,
71
+ type: 'header'
52
72
  };
53
73
 
54
74
  this._addAssertion(assertion);
@@ -72,7 +92,8 @@ class ExpectRequest extends Request {
72
92
  let assertion = {
73
93
  fn: this._assertSnapshot,
74
94
  properties: properties,
75
- field: 'headers'
95
+ field: 'headers',
96
+ type: 'header'
76
97
  };
77
98
 
78
99
  this._addAssertion(assertion);
@@ -87,7 +108,7 @@ class ExpectRequest extends Request {
87
108
  }
88
109
 
89
110
  try {
90
- if (this.assertions) {
111
+ if (this.assertions && this.assertions.length) {
91
112
  this._assertAll(response);
92
113
  }
93
114
  return callback(null, response);
@@ -97,12 +118,49 @@ class ExpectRequest extends Request {
97
118
  });
98
119
  }
99
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
+ */
100
137
  _assertAll(response) {
138
+ const statusCodeAssertions = [];
139
+ const headerAssertions = [];
140
+
101
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) {
102
156
  assertion.fn(response, assertion);
103
157
  }
104
158
  }
105
159
 
160
+ /**
161
+ * Adds assertion to the queue of assertions to be performed
162
+ * @param {ExpressTestAssertion} assertion
163
+ */
106
164
  _addAssertion(assertion) {
107
165
  // We create the error here so that we get a useful stack trace
108
166
  let error = new assert.AssertionError({
@@ -114,7 +172,6 @@ class ExpectRequest extends Request {
114
172
  error.contextString = this.reqOptions.toString();
115
173
  assertion.error = error;
116
174
 
117
- this.assertions = this.assertions || [];
118
175
  this.assertions.push(assertion);
119
176
  }
120
177
 
@@ -122,6 +179,11 @@ class ExpectRequest extends Request {
122
179
  const {error} = assertion;
123
180
 
124
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
+
125
187
  error.actual = response.statusCode;
126
188
 
127
189
  assert.equal(response.statusCode, assertion.expected, 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/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@tryghost/express-test",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "repository": "https://github.com/TryGhost/framework/tree/main/packages/express-test",
5
5
  "author": "Ghost Foundation",
6
6
  "license": "MIT",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
9
  "dev": "echo \"Implement me!\"",
10
- "test": "NODE_ENV=testing c8 mocha --require ./test/utils/overrides.js './test/**/*.test.js'",
10
+ "test": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha --require ./test/utils/overrides.js './test/**/*.test.js'",
11
11
  "lint": "eslint . --ext .js --cache",
12
12
  "posttest": "yarn lint"
13
13
  },
@@ -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",
25
- "mocha": "9.2.0",
23
+ "express": "4.17.3",
24
+ "express-session": "1.17.2",
25
+ "mocha": "9.2.1",
26
26
  "sinon": "13.0.1"
27
27
  },
28
28
  "dependencies": {
29
- "@tryghost/jest-snapshot": "^0.3.0",
29
+ "@tryghost/jest-snapshot": "^0.3.3",
30
30
  "cookiejar": "^2.1.3",
31
- "reqresnext": "^1.6.6"
31
+ "reqresnext": "^1.7.0"
32
32
  },
33
- "gitHead": "5304fb34a2b5dd02f96386fceed34124940ce9ec"
33
+ "gitHead": "64606b3f53d3ec8e6bf988f694d9dc4bbf5fac70"
34
34
  }