@aimeloic/monkey-tester 5.0.3 → 5.0.4
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/monkey.js +117 -49
- package/package.json +1 -1
package/monkey.js
CHANGED
|
@@ -1,75 +1,129 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
import { UI } from './htmlTemplate.js';
|
|
3
|
+
import { UI, encodePayload } from './htmlTemplate.js';
|
|
4
4
|
|
|
5
5
|
function inferType(name) {
|
|
6
6
|
const n = name.toLowerCase();
|
|
7
|
-
if (n.includes('email'))
|
|
8
|
-
if (n.includes('password') || n.includes('pass'))
|
|
9
|
-
if (n.includes('date'))
|
|
10
|
-
if (n.includes('
|
|
7
|
+
if (n.includes('email')) return 'email';
|
|
8
|
+
if (n.includes('password') || n.includes('pass')) return 'password';
|
|
9
|
+
if (n.includes('date') || n.includes('birth')) return 'date';
|
|
10
|
+
if (n.includes('phone') || n.includes('tel')) return 'tel';
|
|
11
|
+
if (n.includes('url') || n.includes('website') || n.includes('link')) return 'url';
|
|
12
|
+
if (
|
|
13
|
+
n.includes('age') || n.includes('price') || n.includes('amount') ||
|
|
14
|
+
n.includes('count') || n.includes('qty') || n.includes('quantity') ||
|
|
15
|
+
n.includes('stock') || n.includes('salary') || n.includes('total') ||
|
|
16
|
+
(n === 'id') || n.endsWith('_id') || n.endsWith('Id')
|
|
17
|
+
) return 'number';
|
|
11
18
|
return 'text';
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
function buildField(name) {
|
|
15
|
-
return {
|
|
22
|
+
return {
|
|
23
|
+
name,
|
|
24
|
+
label: name.charAt(0).toUpperCase() + name.slice(1).replace(/([A-Z])/g, ' $1'),
|
|
25
|
+
type: inferType(name),
|
|
26
|
+
placeholder: 'Enter ' + name
|
|
27
|
+
};
|
|
16
28
|
}
|
|
17
29
|
|
|
18
30
|
function extractBodyFields(handler) {
|
|
19
31
|
try {
|
|
20
32
|
const source = handler.toString();
|
|
21
33
|
if (!source || source.includes('[native code]')) return [];
|
|
34
|
+
|
|
22
35
|
const seen = new Map();
|
|
23
36
|
const destructRe = /(?:const|let|var)\s*\{\s*([^}]+)\s*\}\s*=\s*req\.body/g;
|
|
24
37
|
let m;
|
|
25
38
|
while ((m = destructRe.exec(source)) !== null) {
|
|
26
|
-
m[1].split(',').forEach(part
|
|
39
|
+
m[1].split(',').forEach(function(part) {
|
|
27
40
|
const name = part.split(':')[0].split('=')[0].trim();
|
|
28
|
-
if (name && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) && !seen.has(name))
|
|
41
|
+
if (name && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) && !seen.has(name)) {
|
|
42
|
+
seen.set(name, buildField(name));
|
|
43
|
+
}
|
|
29
44
|
});
|
|
30
45
|
}
|
|
31
|
-
|
|
46
|
+
|
|
47
|
+
const accessRe = /req\.body\.([a-zA-Z_$][a-zA-Z0-9_$]*)|req\.body\[['"]([a-zA-Z_$][a-zA-Z0-9_$]*)['"]]/g;
|
|
32
48
|
while ((m = accessRe.exec(source)) !== null) {
|
|
33
|
-
const name = m[1]
|
|
49
|
+
const name = m[1] || m[2];
|
|
50
|
+
if (name && !seen.has(name)) seen.set(name, buildField(name));
|
|
34
51
|
}
|
|
52
|
+
|
|
35
53
|
return Array.from(seen.values());
|
|
36
|
-
} catch {
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function fallbackFields(path) {
|
|
60
|
+
const p = path.toLowerCase();
|
|
61
|
+
if (p.includes('login') || p.includes('signin') || p.includes('auth/login')) return ['email', 'password'].map(buildField);
|
|
62
|
+
if (p.includes('register') || p.includes('signup') || p.includes('auth/register')) return ['username', 'email', 'password'].map(buildField);
|
|
63
|
+
if (p.includes('user')) return ['username', 'email', 'password'].map(buildField);
|
|
64
|
+
if (p.includes('product')) return ['name', 'price', 'stock'].map(buildField);
|
|
65
|
+
if (p.includes('order')) return ['productId', 'quantity', 'address'].map(buildField);
|
|
66
|
+
return [];
|
|
37
67
|
}
|
|
38
68
|
|
|
39
69
|
function extractRouterPrefix(layer) {
|
|
40
70
|
if (!layer.regexp) return '';
|
|
41
|
-
const
|
|
42
|
-
|
|
71
|
+
const src = layer.regexp.source;
|
|
72
|
+
const patterns = [/^\^\\\/([^\\?$]+)/, /^\^\\\/([a-zA-Z0-9_/-]+)/];
|
|
73
|
+
for (var i = 0; i < patterns.length; i++) {
|
|
74
|
+
const m = patterns[i].exec(src);
|
|
75
|
+
if (m && m[1]) return '/' + m[1].replace(/\\\//g, '/').replace(/\\/g, '');
|
|
76
|
+
}
|
|
77
|
+
return '';
|
|
43
78
|
}
|
|
44
79
|
|
|
45
|
-
function parseStack(stack, detectedEndpoints, prefix
|
|
80
|
+
function parseStack(stack, detectedEndpoints, prefix) {
|
|
46
81
|
if (!Array.isArray(stack)) return;
|
|
47
|
-
|
|
82
|
+
|
|
83
|
+
for (var i = 0; i < stack.length; i++) {
|
|
84
|
+
var layer = stack[i];
|
|
48
85
|
if (layer.route) {
|
|
49
|
-
|
|
50
|
-
|
|
86
|
+
var rawPath = typeof layer.route.path === 'string' ? layer.route.path : (layer.route.path ? String(layer.route.path) : '');
|
|
87
|
+
var fullPath = (prefix + rawPath).replace(/\/+/g, '/') || '/';
|
|
88
|
+
|
|
51
89
|
if (fullPath.startsWith('/api/tester')) continue;
|
|
52
90
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
while ((pm = paramRe.exec(fullPath)) !== null) {
|
|
61
|
-
pathParams.push({ name: pm[1], label: pm[1].charAt(0).toUpperCase() + pm[1].slice(1), placeholder: 'value' });
|
|
62
|
-
}
|
|
91
|
+
var methods = Object.keys(layer.route.methods || {});
|
|
92
|
+
var pathParams = [];
|
|
93
|
+
var paramRe = /:([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
|
|
94
|
+
var pm;
|
|
95
|
+
while ((pm = paramRe.exec(fullPath)) !== null) {
|
|
96
|
+
pathParams.push({ name: pm[1], label: pm[1].charAt(0).toUpperCase() + pm[1].slice(1), placeholder: 'value' });
|
|
97
|
+
}
|
|
63
98
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
bodyFields = bodyFields.filter(f => !seen.has(f.name) && seen.set(f.name, true));
|
|
99
|
+
var bodyFields = [];
|
|
100
|
+
if (methods.some(function(m) { return ['POST', 'PUT', 'PATCH'].indexOf(m.toUpperCase()) !== -1; })) {
|
|
101
|
+
var handlers = (layer.route.stack || []).map(function(sl) { return sl.handle; }).filter(Boolean);
|
|
102
|
+
for (var j = 0; j < handlers.length; j++) {
|
|
103
|
+
bodyFields.push.apply(bodyFields, extractBodyFields(handlers[j]));
|
|
70
104
|
}
|
|
105
|
+
var seen2 = new Map();
|
|
106
|
+
bodyFields = bodyFields.filter(function(f) {
|
|
107
|
+
if (seen2.has(f.name)) return false;
|
|
108
|
+
seen2.set(f.name, true);
|
|
109
|
+
return true;
|
|
110
|
+
});
|
|
111
|
+
if (bodyFields.length === 0 && pathParams.length === 0) {
|
|
112
|
+
bodyFields = fallbackFields(fullPath);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
71
115
|
|
|
72
|
-
|
|
116
|
+
for (var k = 0; k < methods.length; k++) {
|
|
117
|
+
var httpMethod = methods[k].toUpperCase();
|
|
118
|
+
var key = httpMethod + '::' + fullPath;
|
|
119
|
+
detectedEndpoints[key] = {
|
|
120
|
+
method: httpMethod,
|
|
121
|
+
path: fullPath,
|
|
122
|
+
title: httpMethod + ' ' + fullPath,
|
|
123
|
+
desc: 'Auto-discovered endpoint — ' + fullPath,
|
|
124
|
+
params: pathParams,
|
|
125
|
+
fields: bodyFields
|
|
126
|
+
};
|
|
73
127
|
}
|
|
74
128
|
} else if (layer.name === 'router' && layer.handle && layer.handle.stack) {
|
|
75
129
|
parseStack(layer.handle.stack, detectedEndpoints, prefix + extractRouterPrefix(layer));
|
|
@@ -79,26 +133,40 @@ function parseStack(stack, detectedEndpoints, prefix = '') {
|
|
|
79
133
|
|
|
80
134
|
export function endtesterExpress() {
|
|
81
135
|
return function monkeyTesterMiddleware(req, res, next) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const b64 = Buffer.from(JSON.stringify(detectedEndpoints)).toString('base64');
|
|
136
|
+
var normalized = req.path.replace(/\/+$/, '');
|
|
137
|
+
|
|
138
|
+
// Tester sandbox — must be checked before static pages so query strings don't collide
|
|
139
|
+
if (normalized.startsWith('/api/tester')) {
|
|
140
|
+
var app = req.app;
|
|
141
|
+
var eps = app.__monkey_endpoints_cache__ || {};
|
|
142
|
+
if (!app.__monkey_endpoints_cache__) {
|
|
143
|
+
eps = {};
|
|
144
|
+
var raw = (app._router && app._router.stack) || (app.router && app.router.stack) || [];
|
|
145
|
+
parseStack(raw, eps);
|
|
146
|
+
app.__monkey_endpoints_cache__ = eps;
|
|
147
|
+
}
|
|
148
|
+
var b64 = encodePayload(eps);
|
|
96
149
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
97
150
|
return res.send(UI.tester(b64));
|
|
98
151
|
}
|
|
99
152
|
|
|
153
|
+
// Standalone HTML pages — resolved dynamically against discovered routes
|
|
154
|
+
if (normalized === '/login') return res.send(UI.login());
|
|
155
|
+
if (normalized === '/signup') return res.send(UI.signup());
|
|
156
|
+
if (normalized === '/dashboard') {
|
|
157
|
+
if (!app.__monkey_endpoints_cache__) {
|
|
158
|
+
var eps2 = {};
|
|
159
|
+
var raw2 = (app._router && app._router.stack) || (app.router && app.router.stack) || [];
|
|
160
|
+
parseStack(raw2, eps2);
|
|
161
|
+
app.__monkey_endpoints_cache__ = eps2;
|
|
162
|
+
}
|
|
163
|
+
var b64dash = encodePayload(app.__monkey_endpoints_cache__);
|
|
164
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
165
|
+
return res.send(UI.dashboard(b64dash));
|
|
166
|
+
}
|
|
167
|
+
|
|
100
168
|
next();
|
|
101
169
|
};
|
|
102
170
|
}
|
|
103
171
|
|
|
104
|
-
export default { endtesterExpress };
|
|
172
|
+
export default { endtesterExpress };
|