@contrast/route-coverage 1.20.7 → 1.22.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/lib/index.js +0 -1
- package/lib/index.test.js +129 -0
- package/lib/install/express.js +1 -0
- package/lib/install/express.test.js +418 -0
- package/lib/install/fastify.js +94 -77
- package/lib/install/fastify.test.js +203 -0
- package/lib/install/hapi.js +9 -2
- package/lib/install/hapi.test.js +125 -0
- package/lib/install/koa.js +53 -33
- package/lib/install/koa.test.js +152 -0
- package/lib/install/restify.js +2 -1
- package/lib/install/restify.test.js +92 -0
- package/lib/utils/methods.js +45 -0
- package/package.json +2 -2
package/lib/install/fastify.js
CHANGED
|
@@ -12,99 +12,116 @@
|
|
|
12
12
|
* engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
13
|
* way not consistent with the End User License Agreement.
|
|
14
14
|
*/
|
|
15
|
-
// @ts-check
|
|
16
15
|
'use strict';
|
|
17
16
|
|
|
18
|
-
const {
|
|
19
|
-
const {
|
|
17
|
+
const { StringPrototypeToLowerCase, isString } = require('@contrast/common');
|
|
18
|
+
const { getFastifyMethods } = require('./../utils/methods');
|
|
19
|
+
const { patchType } = require('./../utils/route-info');
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @param {import('..').Core & {
|
|
25
|
-
* routeCoverage: import('..').RouteCoverage & {
|
|
26
|
-
* fastify?: import('@contrast/common').Installable
|
|
27
|
-
* }
|
|
28
|
-
* }} core
|
|
29
|
-
*/
|
|
21
|
+
// Spec: https://contrast.atlassian.net/wiki/spaces/NOD/pages/3454861621/Node.js+Agent+Route+Signatures#Fastify
|
|
30
22
|
module.exports = function init(core) {
|
|
23
|
+
/*
|
|
24
|
+
Fastify has two ways of defining routes:
|
|
25
|
+
1. fastify.route({ method: '<method>', url: '<url>', handler: <fn> })
|
|
26
|
+
2. fastify.<method>('<url>', <fn>)
|
|
27
|
+
(See NODE-3483 for more detail)
|
|
28
|
+
We need a way to keep track of which routes were fully declared using .route
|
|
29
|
+
So, we instrument the route method below and add identifying info to this array
|
|
30
|
+
*/
|
|
31
|
+
const fullyDeclaredRoutes = [];
|
|
31
32
|
const { patcher, depHooks, routeCoverage } = core;
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
function
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (Array.isArray(method)) {
|
|
39
|
-
method.forEach((method) => {
|
|
40
|
-
emitRouteCoverage(url, method);
|
|
41
|
-
});
|
|
42
|
-
} else {
|
|
43
|
-
emitRouteCoverage(url, method);
|
|
34
|
+
const isFullyDeclared = (method, url) => isString(method) && fullyDeclaredRoutes.includes(method + url);
|
|
35
|
+
function fullyDeclare(method, url) {
|
|
36
|
+
if (isString(method)) {
|
|
37
|
+
fullyDeclaredRoutes.push(method + url);
|
|
38
|
+
if (method === 'GET') fullyDeclaredRoutes.push(`HEAD${url}`);
|
|
44
39
|
}
|
|
45
|
-
|
|
46
|
-
// TODO
|
|
47
|
-
observationListener(routeOptions, url);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* @param {RouteOptions} routeOptions
|
|
52
|
-
* @param {string} url
|
|
53
|
-
*/
|
|
54
|
-
function observationListener(routeOptions, url) {
|
|
55
|
-
patcher.patch(routeOptions, 'handler', {
|
|
56
|
-
name: 'fastify.routeOptions.handler',
|
|
57
|
-
patchType: 'route-coverage',
|
|
58
|
-
pre(data) {
|
|
59
|
-
const [request] = data.args;
|
|
60
|
-
const { method } = request.raw;
|
|
61
|
-
const [parsedUrl] = request.url.split(/\?/);
|
|
62
|
-
emitObservation(parsedUrl, url, method);
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
40
|
}
|
|
66
41
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
* @param {string} method
|
|
70
|
-
*/
|
|
71
|
-
function emitRouteCoverage(url, method) {
|
|
42
|
+
function createRouteInfo(method, url, routePath) {
|
|
43
|
+
const fullyDeclared = isFullyDeclared(method, routePath);
|
|
72
44
|
method = StringPrototypeToLowerCase.call(method);
|
|
73
|
-
const event = { signature: createSignature(url, method), url, method, normalizedUrl: url, framework: 'fastify' };
|
|
74
|
-
routeCoverage.discover(event);
|
|
75
|
-
}
|
|
76
45
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
46
|
+
const signature = fullyDeclared
|
|
47
|
+
? `fastify.route({ method: '${method}', url: '${url}', handler: [Function] })`
|
|
48
|
+
: `fastify.${method}('${url}', [Function])`;
|
|
49
|
+
|
|
50
|
+
const routeInfo = {
|
|
51
|
+
signature,
|
|
52
|
+
url,
|
|
53
|
+
method,
|
|
54
|
+
normalizedUrl: url,
|
|
55
|
+
framework: 'fastify'
|
|
56
|
+
};
|
|
57
|
+
return routeInfo;
|
|
85
58
|
}
|
|
86
59
|
|
|
87
60
|
return core.routeCoverage.fastify = {
|
|
88
61
|
install() {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
62
|
+
// The routePath property used below was introduced in 3.2.0
|
|
63
|
+
return depHooks.resolve({ name: 'fastify', version: '>=3.2.0' }, (fastify) => patcher.patch(fastify, {
|
|
64
|
+
name: 'fastify',
|
|
65
|
+
patchType,
|
|
66
|
+
post({ result: server }) {
|
|
67
|
+
patcher.patch(server, 'route', {
|
|
68
|
+
name: 'server.route',
|
|
69
|
+
patchType,
|
|
70
|
+
pre({ args }) {
|
|
71
|
+
const [{ method, url }] = args;
|
|
72
|
+
if (!isString(url)) return;
|
|
73
|
+
|
|
74
|
+
if (Array.isArray(method)) {
|
|
75
|
+
method.forEach((verb) => {
|
|
76
|
+
fullyDeclare(verb, url);
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
fullyDeclare(method, url);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
server.addHook('onRoute', (routeOptions) => {
|
|
84
|
+
if (!routeOptions) return;
|
|
85
|
+
|
|
86
|
+
const { method, url, routePath } = routeOptions;
|
|
87
|
+
if (!method || !url || !routePath || !isString(routePath)) return;
|
|
88
|
+
|
|
89
|
+
let routeInfo;
|
|
90
|
+
const FASTIFY_METHODS = getFastifyMethods(server.version);
|
|
91
|
+
if (Array.isArray(method)) {
|
|
92
|
+
// If a route was defined using .all this method property will be an
|
|
93
|
+
// array of all methods supported by Fastify
|
|
94
|
+
if (FASTIFY_METHODS.every(m => method.includes(m))) {
|
|
95
|
+
routeInfo = createRouteInfo('all', url, routePath);
|
|
96
|
+
routeCoverage.discover(routeInfo);
|
|
97
|
+
} else {
|
|
98
|
+
method.forEach((verb) => {
|
|
99
|
+
routeInfo = createRouteInfo(verb, url, routePath);
|
|
100
|
+
routeCoverage.discover(routeInfo);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
routeInfo = createRouteInfo(method, url, routePath);
|
|
105
|
+
routeCoverage.discover(routeInfo);
|
|
106
|
+
}
|
|
100
107
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
patcher.patch(routeOptions, 'handler', {
|
|
109
|
+
name: 'fastify.routeOptions.handler',
|
|
110
|
+
patchType,
|
|
111
|
+
pre({ args }) {
|
|
112
|
+
const [req] = args;
|
|
113
|
+
const method = StringPrototypeToLowerCase.call(req.raw?.method);
|
|
114
|
+
const [url] = req.url.split(/\?/);
|
|
115
|
+
routeCoverage.observe({ ...routeInfo, url, method });
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
server.addHook('onReady', (done) => {
|
|
120
|
+
routeCoverage.discoveryFinished();
|
|
121
|
+
return done();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}));
|
|
108
125
|
}
|
|
109
126
|
};
|
|
110
127
|
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { expect } = require('chai');
|
|
4
|
+
const sinon = require('sinon');
|
|
5
|
+
const scopes = require('@contrast/scopes');
|
|
6
|
+
const patcher = require('@contrast/patcher');
|
|
7
|
+
const mocks = require('@contrast/test/mocks');
|
|
8
|
+
const { getFastifyMethods } = require('../utils/methods');
|
|
9
|
+
|
|
10
|
+
describe('route-coverage fastify', function () {
|
|
11
|
+
let core, serverMock, framework;
|
|
12
|
+
|
|
13
|
+
beforeEach(function () {
|
|
14
|
+
core = mocks.core();
|
|
15
|
+
core.logger = mocks.logger();
|
|
16
|
+
core.routeCoverage = mocks.routeCoverage();
|
|
17
|
+
core.scopes = scopes(core);
|
|
18
|
+
core.depHooks = mocks.depHooks();
|
|
19
|
+
core.patcher = patcher(core);
|
|
20
|
+
|
|
21
|
+
serverMock = {
|
|
22
|
+
addHook: sinon.stub(),
|
|
23
|
+
route: sinon.stub(),
|
|
24
|
+
version: '4.4.0'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
let fastify;
|
|
28
|
+
framework = 'fastify';
|
|
29
|
+
core.depHooks.resolve.callsFake((desc, cb) => {
|
|
30
|
+
fastify = cb(() => serverMock);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
require('./fastify')(core).install();
|
|
34
|
+
|
|
35
|
+
fastify();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('skips instrumenting if route options does not exist', function () {
|
|
39
|
+
serverMock.addHook.withArgs('onRoute').yield();
|
|
40
|
+
expect(core.routeCoverage.discover).not.to.have.been.called;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('skips instrumenting if there is no method provided', function () {
|
|
44
|
+
serverMock.addHook.withArgs('onRoute').yield({});
|
|
45
|
+
expect(core.routeCoverage.discover).not.to.have.been.called;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('skips instrumenting if there is no url provided', function () {
|
|
49
|
+
serverMock.addHook.withArgs('onRoute').yield({ method: 'GET' });
|
|
50
|
+
expect(core.routeCoverage.discover).not.to.have.been.called;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('discovers a route with a single method', function () {
|
|
54
|
+
serverMock.addHook
|
|
55
|
+
.withArgs('onRoute')
|
|
56
|
+
.yield({ method: 'GET', url: '/test/route', routePath: '/test/route' });
|
|
57
|
+
|
|
58
|
+
expect(core.routeCoverage.discover).to.have.been.calledWith({
|
|
59
|
+
signature: "fastify.get('/test/route', [Function])",
|
|
60
|
+
url: '/test/route',
|
|
61
|
+
normalizedUrl: '/test/route',
|
|
62
|
+
method: 'get',
|
|
63
|
+
framework
|
|
64
|
+
});
|
|
65
|
+
expect(core.routeCoverage.discover).to.have.been.called;
|
|
66
|
+
expect(core.routeCoverage.discover).to.have.callCount(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('discovers a fully declared (.route) route with a single method', function () {
|
|
70
|
+
serverMock.route({ method: 'GET', url: '/test/route' });
|
|
71
|
+
serverMock.addHook
|
|
72
|
+
.withArgs('onRoute')
|
|
73
|
+
.yield({ method: 'GET', url: '/test/route', routePath: '/test/route' });
|
|
74
|
+
|
|
75
|
+
expect(core.routeCoverage.discover).to.have.been.calledWith({
|
|
76
|
+
signature: "fastify.route({ method: 'get', url: '/test/route', handler: [Function] })",
|
|
77
|
+
url: '/test/route',
|
|
78
|
+
normalizedUrl: '/test/route',
|
|
79
|
+
method: 'get',
|
|
80
|
+
framework
|
|
81
|
+
});
|
|
82
|
+
expect(core.routeCoverage.discover).to.have.been.called;
|
|
83
|
+
expect(core.routeCoverage.discover).to.have.callCount(1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('discovers a route with multiple methods', function () {
|
|
87
|
+
serverMock.addHook.withArgs('onRoute').yield({
|
|
88
|
+
method: ['GET', 'POST'],
|
|
89
|
+
url: '/test/route',
|
|
90
|
+
routePath: '/test/route'
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(core.routeCoverage.discover).to.have.been.calledWith({
|
|
94
|
+
signature: "fastify.get('/test/route', [Function])",
|
|
95
|
+
url: '/test/route',
|
|
96
|
+
normalizedUrl: '/test/route',
|
|
97
|
+
method: 'get',
|
|
98
|
+
framework
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(core.routeCoverage.discover).to.have.been.calledWith({
|
|
102
|
+
signature: "fastify.post('/test/route', [Function])",
|
|
103
|
+
url: '/test/route',
|
|
104
|
+
normalizedUrl: '/test/route',
|
|
105
|
+
method: 'post',
|
|
106
|
+
framework
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(core.routeCoverage.discover).to.have.callCount(2);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('discovers a route with all methods', function () {
|
|
113
|
+
const FASTIFY_METHODS = getFastifyMethods('4.4.0');
|
|
114
|
+
serverMock.addHook.withArgs('onRoute').yield({
|
|
115
|
+
method: FASTIFY_METHODS,
|
|
116
|
+
url: '/test/route',
|
|
117
|
+
routePath: '/test/route'
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(core.routeCoverage.discover).to.have.been.calledWith({
|
|
121
|
+
signature: "fastify.all('/test/route', [Function])",
|
|
122
|
+
url: '/test/route',
|
|
123
|
+
normalizedUrl: '/test/route',
|
|
124
|
+
method: 'all',
|
|
125
|
+
framework
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('discovers a fully declared route (.route) with multiple methods', function () {
|
|
130
|
+
serverMock.route({ method: ['GET', 'POST'], url: '/test/route' });
|
|
131
|
+
|
|
132
|
+
serverMock.addHook.withArgs('onRoute').yield({
|
|
133
|
+
method: ['GET', 'POST'],
|
|
134
|
+
url: '/test/route',
|
|
135
|
+
routePath: '/test/route'
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(core.routeCoverage.discover).to.have.been.calledWith({
|
|
139
|
+
signature: "fastify.route({ method: 'get', url: '/test/route', handler: [Function] })",
|
|
140
|
+
url: '/test/route',
|
|
141
|
+
normalizedUrl: '/test/route',
|
|
142
|
+
method: 'get',
|
|
143
|
+
framework
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(core.routeCoverage.discover).to.have.been.calledWith({
|
|
147
|
+
signature: "fastify.route({ method: 'post', url: '/test/route', handler: [Function] })",
|
|
148
|
+
url: '/test/route',
|
|
149
|
+
normalizedUrl: '/test/route',
|
|
150
|
+
method: 'post',
|
|
151
|
+
framework
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(core.routeCoverage.discover).to.have.callCount(2);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('signals that discovery is finished when the onReady hook is triggered', function () {
|
|
158
|
+
serverMock.addHook.withArgs('onReady').yield(sinon.stub());
|
|
159
|
+
|
|
160
|
+
expect(core.routeCoverage.discoveryFinished).to.have.been.calledOnce;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('observes a route when the route handler is called', function () {
|
|
164
|
+
const routeOptions = {
|
|
165
|
+
method: 'GET',
|
|
166
|
+
url: '/test/route',
|
|
167
|
+
routePath: '/test/route',
|
|
168
|
+
handler: sinon.stub(),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
serverMock.addHook.withArgs('onRoute').yield(routeOptions);
|
|
172
|
+
routeOptions.handler({ raw: { method: 'GET' }, url: '/test/route' });
|
|
173
|
+
|
|
174
|
+
expect(core.routeCoverage.observe).to.have.been.calledWith({
|
|
175
|
+
signature: "fastify.get('/test/route', [Function])",
|
|
176
|
+
url: '/test/route',
|
|
177
|
+
method: 'get',
|
|
178
|
+
normalizedUrl: '/test/route',
|
|
179
|
+
framework: 'fastify'
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('removes query string from url before reporting observation', function () {
|
|
184
|
+
const routeOptions = {
|
|
185
|
+
method: 'GET',
|
|
186
|
+
url: '/test/route',
|
|
187
|
+
routePath: '/test/route',
|
|
188
|
+
handler: sinon.stub(),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
serverMock.addHook.withArgs('onRoute').yield(routeOptions);
|
|
192
|
+
routeOptions.handler({ raw: { method: 'GET' }, url: '/test/route?input=foo' });
|
|
193
|
+
|
|
194
|
+
expect(core.routeCoverage.observe).to.have.been.calledWith({
|
|
195
|
+
signature: "fastify.get('/test/route', [Function])",
|
|
196
|
+
url: '/test/route',
|
|
197
|
+
method: 'get',
|
|
198
|
+
normalizedUrl: '/test/route',
|
|
199
|
+
framework: 'fastify'
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
});
|
package/lib/install/hapi.js
CHANGED
|
@@ -15,14 +15,21 @@
|
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
17
|
const { StringPrototypeToLowerCase } = require('@contrast/common');
|
|
18
|
-
const {
|
|
18
|
+
const { patchType } = require('./../utils/route-info');
|
|
19
19
|
|
|
20
|
+
// Spec: https://contrast.atlassian.net/wiki/spaces/NOD/pages/3454861621/Node.js+Agent+Route+Signatures#Hapi
|
|
20
21
|
module.exports = function init(core) {
|
|
21
22
|
const { patcher, depHooks, routeCoverage } = core;
|
|
22
23
|
|
|
23
24
|
function emitRouteCoverage(url, method) {
|
|
24
25
|
method = StringPrototypeToLowerCase.call(method);
|
|
25
|
-
const event = {
|
|
26
|
+
const event = {
|
|
27
|
+
signature: `server.route({ method: '${method}', path: '${url}', handler: [Function] })`,
|
|
28
|
+
url,
|
|
29
|
+
method,
|
|
30
|
+
normalizedUrl: url,
|
|
31
|
+
framework: 'hapi'
|
|
32
|
+
};
|
|
26
33
|
routeCoverage.discover(event);
|
|
27
34
|
}
|
|
28
35
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { expect } = require('chai');
|
|
4
|
+
const sinon = require('sinon');
|
|
5
|
+
const scopes = require('@contrast/scopes');
|
|
6
|
+
const patcher = require('@contrast/patcher');
|
|
7
|
+
const mocks = require('@contrast/test/mocks');
|
|
8
|
+
|
|
9
|
+
describe('route-coverage hapi', function () {
|
|
10
|
+
let core, route, _core, hapi, framework;
|
|
11
|
+
|
|
12
|
+
beforeEach(function () {
|
|
13
|
+
core = mocks.core();
|
|
14
|
+
core.logger = mocks.logger();
|
|
15
|
+
core.routeCoverage = mocks.routeCoverage();
|
|
16
|
+
core.scopes = scopes(core);
|
|
17
|
+
core.depHooks = mocks.depHooks();
|
|
18
|
+
core.patcher = patcher(core);
|
|
19
|
+
|
|
20
|
+
route = {
|
|
21
|
+
settings: {
|
|
22
|
+
handler: sinon.stub()
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
_core = {
|
|
27
|
+
router: {
|
|
28
|
+
add() {
|
|
29
|
+
return { route };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
hapi = {
|
|
35
|
+
server() {
|
|
36
|
+
return { _core };
|
|
37
|
+
},
|
|
38
|
+
Server() {
|
|
39
|
+
return { _core };
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
framework = 'hapi';
|
|
43
|
+
|
|
44
|
+
core.depHooks.resolve.withArgs({ name: '@hapi/hapi', version: '>=18 <22' }).yields(hapi);
|
|
45
|
+
require('./hapi')(core).install();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
['server', 'Server'].forEach((server) => {
|
|
50
|
+
|
|
51
|
+
it('does not report a non-existent route', function () {
|
|
52
|
+
const hapiServer = hapi[server]();
|
|
53
|
+
hapiServer._core.router.add();
|
|
54
|
+
expect(core.routeCoverage.discover).not.to.have.been.called;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('does not report a route missing a method', function () {
|
|
58
|
+
const hapiServer = hapi[server]();
|
|
59
|
+
hapiServer._core.router.add({ path: '/foo' });
|
|
60
|
+
expect(core.routeCoverage.discover).not.to.have.been.called;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('does not report a route missing a path', function () {
|
|
64
|
+
const hapiServer = hapi[server]();
|
|
65
|
+
hapiServer._core.router.add({ method: 'GET' });
|
|
66
|
+
expect(core.routeCoverage.discover).not.to.have.been.called;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('discovers a route with a single method', function () {
|
|
70
|
+
const hapiServer = hapi[server]();
|
|
71
|
+
hapiServer._core.router.add({ method: 'GET', path: '/foo' });
|
|
72
|
+
|
|
73
|
+
expect(core.routeCoverage.discover).to.have.been.calledWith({
|
|
74
|
+
signature: "server.route({ method: 'get', path: '/foo', handler: [Function] })",
|
|
75
|
+
url: '/foo',
|
|
76
|
+
normalizedUrl: '/foo',
|
|
77
|
+
method: 'get',
|
|
78
|
+
framework
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('discovers a route with multiple methods', function () {
|
|
83
|
+
const hapiServer = hapi[server]();
|
|
84
|
+
hapiServer._core.router.add({ method: ['GET', 'PUT'], path: '/foo' });
|
|
85
|
+
|
|
86
|
+
expect(core.routeCoverage.discover).to.have.been.calledWith({
|
|
87
|
+
signature: "server.route({ method: 'get', path: '/foo', handler: [Function] })",
|
|
88
|
+
url: '/foo',
|
|
89
|
+
normalizedUrl: '/foo',
|
|
90
|
+
method: 'get',
|
|
91
|
+
framework
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(core.routeCoverage.discover).to.have.been.calledWith({
|
|
95
|
+
signature: "server.route({ method: 'put', path: '/foo', handler: [Function] })",
|
|
96
|
+
url: '/foo',
|
|
97
|
+
normalizedUrl: '/foo',
|
|
98
|
+
method: 'put',
|
|
99
|
+
framework
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('observes a route when a route handler is exercised', async function () {
|
|
104
|
+
const hapiServer = hapi[server]();
|
|
105
|
+
const { route } = hapiServer._core.router.add({ method: 'get', path: '/foo' });
|
|
106
|
+
route.settings.handler({ method: 'get', path: '/foo', route: { path: '/foo' } });
|
|
107
|
+
expect(core.routeCoverage.observe).to.have.been.calledWith({
|
|
108
|
+
url: '/foo',
|
|
109
|
+
normalizedUrl: '/foo',
|
|
110
|
+
method: 'get'
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('observes a route when a route contains parameters', async function () {
|
|
115
|
+
const hapiServer = hapi[server]();
|
|
116
|
+
const { route } = hapiServer._core.router.add({ method: 'get', path: '/foo/{id}' });
|
|
117
|
+
route.settings.handler({ method: 'get', path: '/foo/1', route: { path: '/foo/{id}' } });
|
|
118
|
+
expect(core.routeCoverage.observe).to.have.been.calledWith({
|
|
119
|
+
url: '/foo/1',
|
|
120
|
+
normalizedUrl: '/foo/{id}',
|
|
121
|
+
method: 'get'
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
package/lib/install/koa.js
CHANGED
|
@@ -14,56 +14,76 @@
|
|
|
14
14
|
*/
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
|
-
const {
|
|
17
|
+
const { METHODS } = require('./../utils/methods');
|
|
18
|
+
const { StringPrototypeToLowerCase, isString } = require('@contrast/common');
|
|
18
19
|
const { createSignature, patchType } = require('./../utils/route-info');
|
|
19
20
|
|
|
21
|
+
// Spec: https://contrast.atlassian.net/wiki/spaces/NOD/pages/3454861621/Node.js+Agent+Route+Signatures#Koa
|
|
20
22
|
module.exports = function init(core) {
|
|
21
23
|
const { patcher, depHooks, routeCoverage } = core;
|
|
22
24
|
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (req) {
|
|
33
|
-
const { url: reqUrl, method } = req;
|
|
34
|
-
const [url] = reqUrl.split(/\?/);
|
|
35
|
-
routeCoverage.observe({ url, method: StringPrototypeToLowerCase.call(method || ''), normalizedUrl: path });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
await next();
|
|
25
|
+
function createRouteInfo(method, url) {
|
|
26
|
+
method = StringPrototypeToLowerCase.call(method);
|
|
27
|
+
const routeInfo = {
|
|
28
|
+
signature: createSignature(url, method),
|
|
29
|
+
url,
|
|
30
|
+
method,
|
|
31
|
+
normalizedUrl: url,
|
|
32
|
+
framework: 'koa'
|
|
39
33
|
};
|
|
34
|
+
return routeInfo;
|
|
35
|
+
}
|
|
40
36
|
|
|
41
37
|
return core.routeCoverage.koa = {
|
|
42
38
|
install() {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
depHooks.resolve({ name: 'koa', version: '>=2.3.0' }, (Koa) => {
|
|
40
|
+
// Koa uses its own routing library @koa/router to define routes before
|
|
41
|
+
// mounting them on the app with .use so instrumenting use and traversing
|
|
42
|
+
// the constructed routes is the more technically correct approach than
|
|
43
|
+
// instrumenting the routing library
|
|
44
|
+
patcher.patch(Koa.prototype, 'use', {
|
|
45
|
+
name: 'Koa.prototype.use',
|
|
46
|
+
patchType,
|
|
47
|
+
pre: ({ args }) => {
|
|
48
|
+
if (args?.length === 0) return;
|
|
49
|
+
const [router] = args;
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
name: 'koaRouter.prototype',
|
|
49
|
-
patchType,
|
|
50
|
-
post: ({ args, result: layer }) => {
|
|
51
|
-
const [path, methods] = args;
|
|
52
|
-
if (!path || !methods || Array.isArray(path)) {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
51
|
+
if (!router?.router) return;
|
|
55
52
|
|
|
53
|
+
router.router.stack.forEach((Layer) => {
|
|
54
|
+
const { methods, path } = Layer;
|
|
55
|
+
if (!path || !isString(path)) return;
|
|
56
|
+
|
|
57
|
+
let routeInfo;
|
|
56
58
|
if (methods.length === 0) {
|
|
57
|
-
|
|
59
|
+
routeInfo = createRouteInfo('use', path);
|
|
60
|
+
routeCoverage.discover(routeInfo);
|
|
61
|
+
} else if (METHODS.every(m => methods.includes(m))) {
|
|
62
|
+
// If a route was defined using .all this methods property will be an
|
|
63
|
+
// array of all methods supported by Koa
|
|
64
|
+
routeInfo = createRouteInfo('all', path);
|
|
65
|
+
routeCoverage.discover(routeInfo);
|
|
58
66
|
} else {
|
|
59
67
|
methods.forEach((method) => {
|
|
60
|
-
|
|
68
|
+
routeInfo = createRouteInfo(method, path);
|
|
69
|
+
routeCoverage.discover(routeInfo);
|
|
61
70
|
});
|
|
62
71
|
}
|
|
63
72
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
if (!Layer.stack || Layer.stack.length === 0) return;
|
|
74
|
+
async function observationMiddleware(ctx, next) {
|
|
75
|
+
if (!ctx.request) return;
|
|
76
|
+
const { url: reqUrl, method } = ctx.request;
|
|
77
|
+
const [url] = reqUrl.split(/\?/);
|
|
78
|
+
routeCoverage.observe({ ...routeInfo, url, method: StringPrototypeToLowerCase.call(method) });
|
|
79
|
+
await next();
|
|
80
|
+
}
|
|
81
|
+
// If two routes share middleware, the same stack is used
|
|
82
|
+
// To add our observation middleware without adding them to all routes
|
|
83
|
+
// we need to create a shallow copy
|
|
84
|
+
Layer.stack = [observationMiddleware, ...Layer.stack];
|
|
85
|
+
});
|
|
86
|
+
}
|
|
67
87
|
});
|
|
68
88
|
});
|
|
69
89
|
}
|