@contrast/route-coverage 1.6.0 → 1.8.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.d.ts CHANGED
@@ -25,7 +25,7 @@ export interface RouteCoverage extends Installable {
25
25
  discover(info: RouteInfo): void;
26
26
  delete(info: RouteInfo): void;
27
27
  discoveryFinished(): void;
28
- observe(info: Pick<RouteInfo, 'method' | 'url'>): void;
28
+ observe(info: Pick<RouteInfo>): void;
29
29
  }
30
30
 
31
31
  export interface Core {
package/lib/index.js CHANGED
@@ -46,7 +46,7 @@ module.exports = function init(core) {
46
46
  },
47
47
 
48
48
  observe(info) {
49
- const route = routeInfo.get(routeIdentifier(info));
49
+ const route = info.signature ? info : routeInfo.get(routeIdentifier(info));
50
50
 
51
51
  if (!route) {
52
52
  logger.debug(info, 'unable to observe undiscovered route');
@@ -25,8 +25,10 @@ const METHODS = [
25
25
  'options',
26
26
  'head',
27
27
  ];
28
-
29
- const { createSignature, patchType } = require('./../utils/route-info');
28
+ // eslint-disable-next-line node/no-extraneous-require
29
+ const fnInspect = require('@contrast/fn-inspect');
30
+ const { createSignature, patchType } = require('../utils/route-info');
31
+ const { join, replace, toLowerCase } = require('@contrast/common');
30
32
 
31
33
  /**
32
34
  * @param {import('..').Core & {
@@ -37,27 +39,81 @@ const { createSignature, patchType } = require('./../utils/route-info');
37
39
  */
38
40
  module.exports = function init(core) {
39
41
  const routers = new Map();
42
+ const signatureMap = new Map();
40
43
  const { patcher, depHooks, routeCoverage } = core;
41
44
 
42
- function emitRouteCoverage(router, url, method) {
43
- const event = { signature: createSignature(url, method), url, method };
44
- const layers = routers.get(router) || [];
45
- layers.push(event);
46
- routers.set(router, layers);
47
- routeCoverage.discover(event);
45
+ function formatUrl(url) {
46
+ if (Array.isArray(url)) {
47
+ return `/[${join(url, ', ')}]`;
48
+ } else if (url instanceof RegExp) {
49
+ return `/{${replace(url.toString(), /(^\/?)|(\/?$)/g, '')}}`;
50
+ } else {
51
+ return url;
52
+ }
48
53
  }
49
54
 
50
- function handleRouteDiscovery(data, method) {
51
- const [url, fn] = data.args;
52
- if (!url || !fn) {
53
- return;
55
+ function getLastLayer(router) {
56
+ if (router.stack) {
57
+ const len = router.stack.length;
58
+ return router.stack[len - 1];
54
59
  }
55
- if (Array.isArray(url)) {
56
- url.forEach((path) => {
57
- emitRouteCoverage(data.result, path, method);
60
+ }
61
+
62
+ function getLayerHandleMethod(layer) {
63
+ let methodName = 'handle';
64
+ const __handleData = fnInspect.funcInfo(layer.__handle);
65
+ if (__handleData && __handleData.file.includes('express-async-errors')) {
66
+ methodName = '__handle';
67
+ }
68
+ return methodName;
69
+ }
70
+
71
+ function patchHandler(layer, route) {
72
+ if (!layer) return;
73
+ const handle = getLayerHandleMethod(layer);
74
+ patcher.patch(layer, handle, {
75
+ name: 'express.Router.handle',
76
+ patchType,
77
+ post(data) {
78
+ const [req] = data.args;
79
+ const method = req?.method && toLowerCase(req.method);
80
+ const url = `${req.baseUrl}${req._parsedUrl.pathname}`;
81
+ const { signature } = signatureMap.get(route.signature);
82
+ if (method) routeCoverage.observe({ signature, url, method });
83
+ }
84
+ });
85
+ }
86
+
87
+ function createRoute(url, method, id) {
88
+ const signature = createSignature(url, method);
89
+ const route = { signature, url, method, id: id || signature };
90
+ signatureMap.set(signature, route);
91
+ return route;
92
+ }
93
+
94
+ function discoverRoute({ signature, url, method }) {
95
+ routeCoverage.discover({ signature, url, method });
96
+ }
97
+
98
+ function instrumentRoute(router, route) {
99
+ if (!router) return;
100
+ const layer = getLastLayer(router);
101
+ patchHandler(layer, route);
102
+ }
103
+
104
+ function updateRoutes(prefix, router, updatedRouter) {
105
+ const routes = routers.get(router);
106
+ const updatedLayers = router === updatedRouter
107
+ ? [] : (routers.get(updatedRouter) || []);
108
+ if (routes) {
109
+ routes.forEach((route) => {
110
+ const { url, method, id } = route;
111
+ const newRoute = createRoute(`${prefix}${url}`, method, id);
112
+ updatedLayers.push(newRoute);
113
+ signatureMap.set(id, newRoute);
58
114
  });
59
- } else {
60
- emitRouteCoverage(data.result, url, method);
115
+ routers.set(router, updatedLayers);
116
+ routers.set(updatedRouter, updatedLayers);
61
117
  }
62
118
  }
63
119
 
@@ -66,36 +122,29 @@ module.exports = function init(core) {
66
122
  depHooks.resolve(
67
123
  { name: 'express' },
68
124
  (express) => {
69
- patcher.patch(express.Router, 'handle', {
70
- name: 'express.Router.handle',
71
- patchType,
72
- post(data) {
73
- // TODO: Can this handle all route observation?
74
- const [req] = data.args;
75
- const method = req?.method?.toLowerCase();
76
- const url = `${req.baseUrl}${req._parsedUrl.pathname}`;
77
- if (method) routeCoverage.observe({ url, method });
78
- }
79
- });
80
125
 
81
126
  patcher.patch(express.Router, 'use', {
82
127
  name: 'express.Router.use',
83
128
  patchType,
84
129
  post({ args, result }) {
85
130
  const [prefix, router] = args;
86
- const layers = routers.get(router);
87
- const updatedLayers = [];
88
- if (layers) {
89
- layers.forEach((layer) => {
90
- const { url, method } = layer;
91
- const updatedUrl = `${prefix}${url}`;
92
- const event = { signature: createSignature(updatedUrl, method), url: updatedUrl, method };
93
- updatedLayers.push(event);
94
- routeCoverage.delete(layer);
95
- routeCoverage.discover(event);
131
+ if (typeof prefix === 'string' && prefix !== '/') {
132
+ updateRoutes(prefix, router, result);
133
+ }
134
+ }
135
+ });
136
+
137
+ patcher.patch(express.application, 'use', {
138
+ name: 'express.application.use',
139
+ patchType,
140
+ post({ args }) {
141
+ const idx = args.length;
142
+ const router = args[idx - 1];
143
+ const routes = routers.get(router);
144
+ if (routes) {
145
+ routes.forEach((route) => {
146
+ discoverRoute(route);
96
147
  });
97
- routers.delete(router);
98
- routers.set(result, updatedLayers);
99
148
  }
100
149
  }
101
150
  });
@@ -104,16 +153,27 @@ module.exports = function init(core) {
104
153
  patcher.patch(express.application, method, {
105
154
  name: `express.application.${method}`,
106
155
  patchType,
107
- post(data) {
108
- handleRouteDiscovery(data, method);
156
+ post({ args, result }) {
157
+ const [url, fn] = args;
158
+ if (!url || !fn) return;
159
+ const route = createRoute(formatUrl(url), method);
160
+ instrumentRoute(result?._router, route);
161
+ discoverRoute(route);
109
162
  }
110
163
  });
111
164
 
112
165
  patcher.patch(express.Router, method, {
113
166
  name: `express.Router.${method}`,
114
167
  patchType,
115
- post(data) {
116
- handleRouteDiscovery(data, method);
168
+ post({ args, obj: router }) {
169
+ const [url, fn] = args;
170
+ if (!url || !fn) return;
171
+ const route = createRoute(formatUrl(url), method);
172
+ const routes = routers.get(router) || [];
173
+
174
+ instrumentRoute(router, route);
175
+ routes.push(route);
176
+ routers.set(router, routes);
117
177
  }
118
178
  });
119
179
  });
@@ -16,6 +16,7 @@
16
16
  'use strict';
17
17
 
18
18
  const { createSignature } = require('./../utils/route-info');
19
+ const { toLowerCase } = require('@contrast/common');
19
20
 
20
21
  /** @typedef {Parameters<import('fastify3.0.0').onRouteHookHandler>[0]} RouteOptions */
21
22
 
@@ -67,7 +68,7 @@ module.exports = function init(core) {
67
68
  * @param {string} method
68
69
  */
69
70
  function emitRouteCoverage(url, method) {
70
- method = method.toLowerCase();
71
+ method = toLowerCase(method);
71
72
  const event = { signature: createSignature(url, method), url, method };
72
73
  routeCoverage.discover(event);
73
74
  }
@@ -77,7 +78,7 @@ module.exports = function init(core) {
77
78
  * @param {string=} method
78
79
  */
79
80
  function emitObservation(url, method) {
80
- method = method?.toLowerCase();
81
+ method = method && toLowerCase(method);
81
82
  routeCoverage.observe({ method, url });
82
83
  }
83
84
 
@@ -15,6 +15,7 @@
15
15
  'use strict';
16
16
 
17
17
  const { createSignature, patchType } = require('./../utils/route-info');
18
+ const { toLowerCase } = require('@contrast/common');
18
19
 
19
20
  module.exports = function init(core) {
20
21
  const { patcher, depHooks, routeCoverage } = core;
@@ -30,7 +31,7 @@ module.exports = function init(core) {
30
31
 
31
32
  if (req) {
32
33
  const { method } = req;
33
- routeCoverage.observe({ url: path, method: method.toLowerCase() });
34
+ routeCoverage.observe({ url: path, method: toLowerCase(method || '') });
34
35
  }
35
36
 
36
37
  await next();
@@ -50,7 +51,7 @@ module.exports = function init(core) {
50
51
  emitRouteCoverage(path, 'use');
51
52
  } else {
52
53
  methods.forEach((method) => {
53
- emitRouteCoverage(path, method.toLowerCase());
54
+ emitRouteCoverage(path, toLowerCase(method || ''));
54
55
  });
55
56
  }
56
57
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/route-coverage",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
@@ -14,6 +14,6 @@
14
14
  "test": "../scripts/test.sh"
15
15
  },
16
16
  "dependencies": {
17
- "@contrast/common": "1.10.0"
17
+ "@contrast/common": "1.12.0"
18
18
  }
19
19
  }