@contrast/route-coverage 1.6.0 → 1.7.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,9 @@ 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');
30
31
 
31
32
  /**
32
33
  * @param {import('..').Core & {
@@ -37,27 +38,81 @@ const { createSignature, patchType } = require('./../utils/route-info');
37
38
  */
38
39
  module.exports = function init(core) {
39
40
  const routers = new Map();
41
+ const signatureMap = new Map();
40
42
  const { patcher, depHooks, routeCoverage } = core;
41
43
 
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);
44
+ function formatUrl(url) {
45
+ if (Array.isArray(url)) {
46
+ return `/[${url.join(', ')}]`;
47
+ } else if (url instanceof RegExp) {
48
+ return `/{${url.toString().replace(/(^\/?)|(\/?$)/g, '')}}`;
49
+ } else {
50
+ return url;
51
+ }
48
52
  }
49
53
 
50
- function handleRouteDiscovery(data, method) {
51
- const [url, fn] = data.args;
52
- if (!url || !fn) {
53
- return;
54
+ function getLastLayer(router) {
55
+ if (router.stack) {
56
+ const len = router.stack.length;
57
+ return router.stack[len - 1];
54
58
  }
55
- if (Array.isArray(url)) {
56
- url.forEach((path) => {
57
- emitRouteCoverage(data.result, path, method);
59
+ }
60
+
61
+ function getLayerHandleMethod(layer) {
62
+ let methodName = 'handle';
63
+ const __handleData = fnInspect.funcInfo(layer.__handle);
64
+ if (__handleData && __handleData.file.includes('express-async-errors')) {
65
+ methodName = '__handle';
66
+ }
67
+ return methodName;
68
+ }
69
+
70
+ function patchHandler(layer, route) {
71
+ if (!layer) return;
72
+ const handle = getLayerHandleMethod(layer);
73
+ patcher.patch(layer, handle, {
74
+ name: 'express.Router.handle',
75
+ patchType,
76
+ post(data) {
77
+ const [req] = data.args;
78
+ const method = req?.method?.toLowerCase();
79
+ const url = `${req.baseUrl}${req._parsedUrl.pathname}`;
80
+ const { signature } = signatureMap.get(route.signature);
81
+ if (method) routeCoverage.observe({ signature, url, method });
82
+ }
83
+ });
84
+ }
85
+
86
+ function createRoute(url, method, id) {
87
+ const signature = createSignature(url, method);
88
+ const route = { signature, url, method, id: id || signature };
89
+ signatureMap.set(signature, route);
90
+ return route;
91
+ }
92
+
93
+ function discoverRoute({ signature, url, method }) {
94
+ routeCoverage.discover({ signature, url, method });
95
+ }
96
+
97
+ function instrumentRoute(router, route) {
98
+ if (!router) return;
99
+ const layer = getLastLayer(router);
100
+ patchHandler(layer, route);
101
+ }
102
+
103
+ function updateRoutes(prefix, router, updatedRouter) {
104
+ const routes = routers.get(router);
105
+ const updatedLayers = router === updatedRouter
106
+ ? [] : (routers.get(updatedRouter) || []);
107
+ if (routes) {
108
+ routes.forEach((route) => {
109
+ const { url, method, id } = route;
110
+ const newRoute = createRoute(`${prefix}${url}`, method, id);
111
+ updatedLayers.push(newRoute);
112
+ signatureMap.set(id, newRoute);
58
113
  });
59
- } else {
60
- emitRouteCoverage(data.result, url, method);
114
+ routers.set(router, updatedLayers);
115
+ routers.set(updatedRouter, updatedLayers);
61
116
  }
62
117
  }
63
118
 
@@ -66,36 +121,29 @@ module.exports = function init(core) {
66
121
  depHooks.resolve(
67
122
  { name: 'express' },
68
123
  (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
124
 
81
125
  patcher.patch(express.Router, 'use', {
82
126
  name: 'express.Router.use',
83
127
  patchType,
84
128
  post({ args, result }) {
85
129
  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);
130
+ if (typeof prefix === 'string' && prefix !== '/') {
131
+ updateRoutes(prefix, router, result);
132
+ }
133
+ }
134
+ });
135
+
136
+ patcher.patch(express.application, 'use', {
137
+ name: 'express.application.use',
138
+ patchType,
139
+ post({ args }) {
140
+ const idx = args.length;
141
+ const router = args[idx - 1];
142
+ const routes = routers.get(router);
143
+ if (routes) {
144
+ routes.forEach((route) => {
145
+ discoverRoute(route);
96
146
  });
97
- routers.delete(router);
98
- routers.set(result, updatedLayers);
99
147
  }
100
148
  }
101
149
  });
@@ -104,16 +152,27 @@ module.exports = function init(core) {
104
152
  patcher.patch(express.application, method, {
105
153
  name: `express.application.${method}`,
106
154
  patchType,
107
- post(data) {
108
- handleRouteDiscovery(data, method);
155
+ post({ args, result }) {
156
+ const [url, fn] = args;
157
+ if (!url || !fn) return;
158
+ const route = createRoute(formatUrl(url), method);
159
+ instrumentRoute(result?._router, route);
160
+ discoverRoute(route);
109
161
  }
110
162
  });
111
163
 
112
164
  patcher.patch(express.Router, method, {
113
165
  name: `express.Router.${method}`,
114
166
  patchType,
115
- post(data) {
116
- handleRouteDiscovery(data, method);
167
+ post({ args, obj: router }) {
168
+ const [url, fn] = args;
169
+ if (!url || !fn) return;
170
+ const route = createRoute(formatUrl(url), method);
171
+ const routes = routers.get(router) || [];
172
+
173
+ instrumentRoute(router, route);
174
+ routes.push(route);
175
+ routers.set(router, routes);
117
176
  }
118
177
  });
119
178
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/route-coverage",
3
- "version": "1.6.0",
3
+ "version": "1.7.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.11.0"
18
18
  }
19
19
  }