@tryghost/express-test 2.0.3 → 2.2.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 +10 -15
- package/index.js +1 -1
- package/lib/Agent.js +15 -11
- package/lib/ExpectRequest.js +19 -15
- package/lib/Request.js +19 -19
- package/lib/utils.js +1 -1
- package/package.json +17 -17
package/README.md
CHANGED
|
@@ -8,8 +8,7 @@ Rapid express testing without HTTP
|
|
|
8
8
|
|
|
9
9
|
or
|
|
10
10
|
|
|
11
|
-
`
|
|
12
|
-
|
|
11
|
+
`pnpm add @tryghost/express-test`
|
|
13
12
|
|
|
14
13
|
## Purpose
|
|
15
14
|
|
|
@@ -26,7 +25,6 @@ For the most up-to-date and clear usage info, there's a live working example of
|
|
|
26
25
|
|
|
27
26
|
An instantiated agent can make HTTP-like calls, with a supertest-like chained API to set headers & body and to check status, headers and anything else.
|
|
28
27
|
|
|
29
|
-
|
|
30
28
|
```
|
|
31
29
|
const agent = new TestAgent(app)
|
|
32
30
|
|
|
@@ -56,9 +54,10 @@ await agent.logout().get('/protected-route');
|
|
|
56
54
|
|
|
57
55
|
This is an initial version for review. More docs coming if it works :)
|
|
58
56
|
|
|
59
|
-
|
|
60
57
|
### Assertion execution order
|
|
61
|
-
|
|
58
|
+
|
|
59
|
+
The order of _chained_ assertion execution is NOT the order they were declared in. The framework follows the priority order of the assertion types:
|
|
60
|
+
|
|
62
61
|
1. `expect`
|
|
63
62
|
2. `expectHeader`
|
|
64
63
|
3. `expectStatus`
|
|
@@ -69,25 +68,21 @@ The custom order is here to prioritize assertions with the most context first. T
|
|
|
69
68
|
|
|
70
69
|
## Develop
|
|
71
70
|
|
|
72
|
-
This is a mono repository, managed with [
|
|
71
|
+
This is a mono repository, managed with [Nx](https://nx.dev).
|
|
73
72
|
|
|
74
73
|
Follow the instructions for the top-level repo.
|
|
75
|
-
1. `git clone` this repo & `cd` into it as usual
|
|
76
|
-
2. Run `yarn` to install top-level dependencies.
|
|
77
74
|
|
|
75
|
+
1. `git clone` this repo & `cd` into it as usual
|
|
76
|
+
2. Run `pnpm install` to install top-level dependencies.
|
|
78
77
|
|
|
79
78
|
## Run
|
|
80
79
|
|
|
81
|
-
- `
|
|
82
|
-
|
|
80
|
+
- `pnpm dev`
|
|
83
81
|
|
|
84
82
|
## Test
|
|
85
83
|
|
|
86
|
-
- `
|
|
87
|
-
- `
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
- `pnpm lint` runs oxlint
|
|
85
|
+
- `pnpm test` runs lint and tests
|
|
91
86
|
|
|
92
87
|
# Copyright & License
|
|
93
88
|
|
package/index.js
CHANGED
|
@@ -7,5 +7,5 @@ module.exports.snapshot = {
|
|
|
7
7
|
snapshotManager: require('@tryghost/jest-snapshot').snapshotManager,
|
|
8
8
|
matchSnapshotAssertion: require('@tryghost/jest-snapshot').matchSnapshotAssertion,
|
|
9
9
|
any: require('@tryghost/jest-snapshot').any,
|
|
10
|
-
stringMatching: require('@tryghost/jest-snapshot').stringMatching
|
|
10
|
+
stringMatching: require('@tryghost/jest-snapshot').stringMatching,
|
|
11
11
|
};
|
package/lib/Agent.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
const {CookieJar} = require('cookiejar');
|
|
1
|
+
const { CookieJar } = require('cookiejar');
|
|
3
2
|
const ExpectRequest = require('./ExpectRequest');
|
|
4
|
-
const {RequestOptions} = require('./Request');
|
|
5
|
-
const {normalizeURL} = require('./utils');
|
|
3
|
+
const { RequestOptions } = require('./Request');
|
|
4
|
+
const { normalizeURL } = require('./utils');
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* @typedef AgentOptions
|
|
@@ -73,22 +72,22 @@ class Agent {
|
|
|
73
72
|
_mergeOptions(method, url, options = {}) {
|
|
74
73
|
// It doesn't make sense to call this method without these properties
|
|
75
74
|
if (!method) {
|
|
76
|
-
throw new Error('_mergeOptions cannot be called without a method');
|
|
75
|
+
throw new Error('_mergeOptions cannot be called without a method');
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
if (!url) {
|
|
80
|
-
throw new Error('_mergeOptions cannot be called without a url');
|
|
79
|
+
throw new Error('_mergeOptions cannot be called without a url');
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
// urlOptions
|
|
84
|
-
const {baseUrl, queryParams} = options;
|
|
83
|
+
const { baseUrl, queryParams } = options;
|
|
85
84
|
|
|
86
85
|
return new RequestOptions({
|
|
87
86
|
method,
|
|
88
|
-
url: this._makeUrl(url, Object.assign({}, {baseUrl, queryParams})),
|
|
87
|
+
url: this._makeUrl(url, Object.assign({}, { baseUrl, queryParams })),
|
|
89
88
|
headers: Object.assign({}, this.defaults.headers, options.headers),
|
|
90
89
|
// Set this to an empty object for ease, as express.json will do this anyway
|
|
91
|
-
body: Object.assign({}, this.defaults.body, options.body)
|
|
90
|
+
body: Object.assign({}, this.defaults.body, options.body),
|
|
92
91
|
});
|
|
93
92
|
}
|
|
94
93
|
|
|
@@ -105,9 +104,14 @@ class Agent {
|
|
|
105
104
|
['get', 'post', 'put', 'patch', 'delete', 'options', 'head'].forEach((method) => {
|
|
106
105
|
Agent.prototype[method] = function (url, options) {
|
|
107
106
|
if (!url) {
|
|
108
|
-
throw new Error('Cannot make a request without supplying a url');
|
|
107
|
+
throw new Error('Cannot make a request without supplying a url');
|
|
109
108
|
}
|
|
110
|
-
return new ExpectRequest(
|
|
109
|
+
return new ExpectRequest(
|
|
110
|
+
this.app,
|
|
111
|
+
this.jar,
|
|
112
|
+
this._mergeOptions(method.toUpperCase(), url, options),
|
|
113
|
+
this.snapshotManager,
|
|
114
|
+
);
|
|
111
115
|
};
|
|
112
116
|
});
|
|
113
117
|
|
package/lib/ExpectRequest.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const assert = require('assert');
|
|
2
|
-
const {Request, RequestOptions} = require('./Request');
|
|
3
|
-
const {snapshotManager} = require('@tryghost/jest-snapshot');
|
|
2
|
+
const { Request, RequestOptions } = require('./Request');
|
|
3
|
+
const { snapshotManager } = require('@tryghost/jest-snapshot');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @typedef {object} ExpressTestAssertion
|
|
@@ -30,9 +30,8 @@ class ExpectRequest extends Request {
|
|
|
30
30
|
|
|
31
31
|
expect(callback) {
|
|
32
32
|
if (typeof callback !== 'function') {
|
|
33
|
-
// eslint-disable-next-line ghost/ghost-custom/no-native-error
|
|
34
33
|
throw new Error(
|
|
35
|
-
'express-test expect() requires a callback function, did you mean expectStatus or expectHeader?'
|
|
34
|
+
'express-test expect() requires a callback function, did you mean expectStatus or expectHeader?',
|
|
36
35
|
);
|
|
37
36
|
}
|
|
38
37
|
|
|
@@ -47,7 +46,7 @@ class ExpectRequest extends Request {
|
|
|
47
46
|
|
|
48
47
|
const assertion = {
|
|
49
48
|
fn: wrapperFn,
|
|
50
|
-
type: 'body'
|
|
49
|
+
type: 'body',
|
|
51
50
|
};
|
|
52
51
|
|
|
53
52
|
this._addAssertion(assertion);
|
|
@@ -59,7 +58,7 @@ class ExpectRequest extends Request {
|
|
|
59
58
|
const assertion = {
|
|
60
59
|
fn: this._assertStatus,
|
|
61
60
|
expected,
|
|
62
|
-
type: 'status'
|
|
61
|
+
type: 'status',
|
|
63
62
|
};
|
|
64
63
|
|
|
65
64
|
this._addAssertion(assertion);
|
|
@@ -72,7 +71,7 @@ class ExpectRequest extends Request {
|
|
|
72
71
|
fn: this._assertHeader,
|
|
73
72
|
expectedField: expectedField.toLowerCase(),
|
|
74
73
|
expectedValue,
|
|
75
|
-
type: 'header'
|
|
74
|
+
type: 'header',
|
|
76
75
|
};
|
|
77
76
|
|
|
78
77
|
this._addAssertion(assertion);
|
|
@@ -84,7 +83,7 @@ class ExpectRequest extends Request {
|
|
|
84
83
|
const assertion = {
|
|
85
84
|
fn: this._assertEmptyBody,
|
|
86
85
|
expected: {},
|
|
87
|
-
type: 'body'
|
|
86
|
+
type: 'body',
|
|
88
87
|
};
|
|
89
88
|
|
|
90
89
|
this._addAssertion(assertion);
|
|
@@ -97,7 +96,7 @@ class ExpectRequest extends Request {
|
|
|
97
96
|
fn: this._assertSnapshot,
|
|
98
97
|
properties: properties,
|
|
99
98
|
field: 'body',
|
|
100
|
-
type: 'body'
|
|
99
|
+
type: 'body',
|
|
101
100
|
};
|
|
102
101
|
|
|
103
102
|
this._addAssertion(assertion);
|
|
@@ -110,7 +109,7 @@ class ExpectRequest extends Request {
|
|
|
110
109
|
fn: this._assertSnapshot,
|
|
111
110
|
properties: properties,
|
|
112
111
|
field: 'headers',
|
|
113
|
-
type: 'header'
|
|
112
|
+
type: 'header',
|
|
114
113
|
};
|
|
115
114
|
|
|
116
115
|
this._addAssertion(assertion);
|
|
@@ -185,7 +184,7 @@ class ExpectRequest extends Request {
|
|
|
185
184
|
let error = new assert.AssertionError({
|
|
186
185
|
message: 'Unexpected assertion error',
|
|
187
186
|
expected: assertion.expected,
|
|
188
|
-
stackStartFn: this._addAssertion
|
|
187
|
+
stackStartFn: this._addAssertion,
|
|
189
188
|
});
|
|
190
189
|
|
|
191
190
|
error.contextString = this.reqOptions.toString();
|
|
@@ -195,11 +194,16 @@ class ExpectRequest extends Request {
|
|
|
195
194
|
}
|
|
196
195
|
|
|
197
196
|
_assertStatus(response, assertion) {
|
|
198
|
-
const {error} = assertion;
|
|
197
|
+
const { error } = assertion;
|
|
199
198
|
|
|
200
199
|
error.message = `Expected statusCode ${assertion.expected}, got statusCode ${response.statusCode} ${error.contextString}`;
|
|
201
200
|
|
|
202
|
-
if (
|
|
201
|
+
if (
|
|
202
|
+
response.body &&
|
|
203
|
+
response.body.errors &&
|
|
204
|
+
response.body.errors[0] &&
|
|
205
|
+
response.body.errors[0].message
|
|
206
|
+
) {
|
|
203
207
|
error.message += `\n${response.body.errors[0].message}`;
|
|
204
208
|
|
|
205
209
|
if (response.body.errors[0].context) {
|
|
@@ -213,7 +217,7 @@ class ExpectRequest extends Request {
|
|
|
213
217
|
}
|
|
214
218
|
|
|
215
219
|
_assertHeader(response, assertion) {
|
|
216
|
-
const {expectedField, expectedValue, error} = assertion;
|
|
220
|
+
const { expectedField, expectedValue, error } = assertion;
|
|
217
221
|
const actual = response.headers[expectedField];
|
|
218
222
|
|
|
219
223
|
const expectedHeaderString = `${expectedField}: ${expectedValue}`;
|
|
@@ -237,7 +241,7 @@ class ExpectRequest extends Request {
|
|
|
237
241
|
}
|
|
238
242
|
|
|
239
243
|
_assertEmptyBody(response, assertion) {
|
|
240
|
-
const {error, expected} = assertion;
|
|
244
|
+
const { error, expected } = assertion;
|
|
241
245
|
const actual = response.body;
|
|
242
246
|
|
|
243
247
|
error.actual = actual;
|
package/lib/Request.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const http = require('http');
|
|
2
|
-
const {Writable, Readable} = require('stream');
|
|
2
|
+
const { Writable, Readable } = require('stream');
|
|
3
3
|
const express = require('express');
|
|
4
|
-
const {CookieAccessInfo} = require('cookiejar');
|
|
5
|
-
const {parse} = require('url');
|
|
6
|
-
const {isJSON, attachFile} = require('./utils');
|
|
4
|
+
const { CookieAccessInfo } = require('cookiejar');
|
|
5
|
+
const { parse } = require('url');
|
|
6
|
+
const { isJSON, attachFile } = require('./utils');
|
|
7
7
|
|
|
8
8
|
class MockSocket extends Writable {
|
|
9
9
|
constructor() {
|
|
@@ -16,7 +16,7 @@ class MockSocket extends Writable {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
class RequestOptions {
|
|
19
|
-
constructor({method, url, headers, body} = {}) {
|
|
19
|
+
constructor({ method, url, headers, body } = {}) {
|
|
20
20
|
this.method = method || 'GET';
|
|
21
21
|
this.url = url || '/';
|
|
22
22
|
this.headers = headers || {};
|
|
@@ -31,7 +31,8 @@ class RequestOptions {
|
|
|
31
31
|
class Request {
|
|
32
32
|
constructor(app, cookieJar, reqOptions) {
|
|
33
33
|
this.app = app;
|
|
34
|
-
this.reqOptions =
|
|
34
|
+
this.reqOptions =
|
|
35
|
+
reqOptions instanceof RequestOptions ? reqOptions : new RequestOptions(reqOptions);
|
|
35
36
|
this.cookieJar = cookieJar;
|
|
36
37
|
this._formData = null; // Track FormData instance for multiple attachments
|
|
37
38
|
}
|
|
@@ -53,7 +54,7 @@ class Request {
|
|
|
53
54
|
// body is FormData (we cannot reliably check instanceof, not working in all situations)
|
|
54
55
|
const requestBuffer = body.getBuffer();
|
|
55
56
|
const headers = body.getHeaders({
|
|
56
|
-
...this.reqOptions.headers
|
|
57
|
+
...this.reqOptions.headers,
|
|
57
58
|
});
|
|
58
59
|
this.reqOptions.headers = headers;
|
|
59
60
|
return this.body(requestBuffer);
|
|
@@ -110,7 +111,7 @@ class Request {
|
|
|
110
111
|
},
|
|
111
112
|
read: () => {
|
|
112
113
|
return readableStream.read(...arguments);
|
|
113
|
-
}
|
|
114
|
+
},
|
|
114
115
|
};
|
|
115
116
|
return this;
|
|
116
117
|
}
|
|
@@ -149,7 +150,7 @@ class Request {
|
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
_getReqRes() {
|
|
152
|
-
const {app, reqOptions} = this;
|
|
153
|
+
const { app, reqOptions } = this;
|
|
153
154
|
|
|
154
155
|
// Create proper Node.js req/res objects using built-in http module.
|
|
155
156
|
// MockSocket provides a writable stream so that res.end() properly emits 'finish'.
|
|
@@ -159,7 +160,7 @@ class Request {
|
|
|
159
160
|
// When streaming body data, the socket must appear readable so that
|
|
160
161
|
// body-parser's on-finished check doesn't skip the request as "already finished".
|
|
161
162
|
if (hasStreamOverrides) {
|
|
162
|
-
Object.defineProperty(socket, 'readable', {value: true});
|
|
163
|
+
Object.defineProperty(socket, 'readable', { value: true });
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
const req = new http.IncomingMessage(socket);
|
|
@@ -211,12 +212,15 @@ class Request {
|
|
|
211
212
|
if (hasStreamOverrides) {
|
|
212
213
|
const props = Object.keys(this.reqOptions.methodOverrides);
|
|
213
214
|
for (const prop of props) {
|
|
214
|
-
const descriptor = Object.getOwnPropertyDescriptor(
|
|
215
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
216
|
+
this.reqOptions.methodOverrides,
|
|
217
|
+
prop,
|
|
218
|
+
);
|
|
215
219
|
Object.defineProperty(req, prop, descriptor);
|
|
216
220
|
}
|
|
217
221
|
}
|
|
218
222
|
|
|
219
|
-
return {req, res};
|
|
223
|
+
return { req, res };
|
|
220
224
|
}
|
|
221
225
|
|
|
222
226
|
_buildResponse(res) {
|
|
@@ -229,12 +233,12 @@ class Request {
|
|
|
229
233
|
body = text && JSON.parse(text);
|
|
230
234
|
}
|
|
231
235
|
|
|
232
|
-
return {statusCode, headers, text, body, response: res};
|
|
236
|
+
return { statusCode, headers, text, body, response: res };
|
|
233
237
|
}
|
|
234
238
|
|
|
235
239
|
_doRequest(callback) {
|
|
236
240
|
try {
|
|
237
|
-
const {req, res} = this._getReqRes();
|
|
241
|
+
const { req, res } = this._getReqRes();
|
|
238
242
|
|
|
239
243
|
this._restoreCookies(req);
|
|
240
244
|
|
|
@@ -255,11 +259,7 @@ class Request {
|
|
|
255
259
|
_getCookies(req) {
|
|
256
260
|
const url = parse(req.url);
|
|
257
261
|
|
|
258
|
-
const access = new CookieAccessInfo(
|
|
259
|
-
url.hostname,
|
|
260
|
-
url.pathname,
|
|
261
|
-
url.protocol === 'https:'
|
|
262
|
-
);
|
|
262
|
+
const access = new CookieAccessInfo(url.hostname, url.pathname, url.protocol === 'https:');
|
|
263
263
|
|
|
264
264
|
return this.cookieJar.getCookies(access).toValueString();
|
|
265
265
|
}
|
package/lib/utils.js
CHANGED
package/package.json
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tryghost/express-test",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"author": "Ghost Foundation",
|
|
4
6
|
"repository": {
|
|
5
7
|
"type": "git",
|
|
6
8
|
"url": "git+https://github.com/TryGhost/framework.git",
|
|
7
9
|
"directory": "packages/express-test"
|
|
8
10
|
},
|
|
9
|
-
"author": "Ghost Foundation",
|
|
10
|
-
"license": "MIT",
|
|
11
|
-
"main": "index.js",
|
|
12
|
-
"scripts": {
|
|
13
|
-
"dev": "echo \"Implement me!\"",
|
|
14
|
-
"test": "NODE_ENV=testing vitest run --coverage",
|
|
15
|
-
"lint": "eslint . --ext .js --cache",
|
|
16
|
-
"posttest": "yarn lint"
|
|
17
|
-
},
|
|
18
11
|
"files": [
|
|
19
12
|
"index.js",
|
|
20
13
|
"lib"
|
|
21
14
|
],
|
|
15
|
+
"main": "index.js",
|
|
22
16
|
"publishConfig": {
|
|
23
17
|
"access": "public"
|
|
24
18
|
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"cookiejar": "2.1.4",
|
|
21
|
+
"form-data": "4.0.5",
|
|
22
|
+
"mime-types": "3.0.2",
|
|
23
|
+
"@tryghost/jest-snapshot": "2.2.0"
|
|
24
|
+
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"express": "5.2.1",
|
|
27
27
|
"express-session": "1.19.0",
|
|
28
28
|
"multer": "2.1.1",
|
|
29
|
-
"sinon": "21.
|
|
29
|
+
"sinon": "21.1.2"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"express": "^4.0.0 || ^5.0.0"
|
|
33
33
|
},
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
34
|
+
"scripts": {
|
|
35
|
+
"dev": "echo \"Implement me!\"",
|
|
36
|
+
"test": "NODE_ENV=testing vitest run --coverage",
|
|
37
|
+
"lint": "oxlint -c ../../.oxlintrc.json .",
|
|
38
|
+
"posttest": "pnpm run lint"
|
|
39
39
|
}
|
|
40
|
-
}
|
|
40
|
+
}
|