@aimeloic/monkey-tester 1.0.0 → 1.0.2
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/htmlTemplate.js +40 -45
- package/index.js +49 -64
- package/package.json +1 -1
package/htmlTemplate.js
CHANGED
|
@@ -112,17 +112,14 @@ export function getHtmlTemplate(endpoints) {
|
|
|
112
112
|
<div id="toast"></div>
|
|
113
113
|
|
|
114
114
|
<script>
|
|
115
|
-
//
|
|
116
|
-
const ENDPOINTS = ${JSON.stringify(endpoints
|
|
115
|
+
// Safely parse the object passed from node environment to prevent script token breaking
|
|
116
|
+
const ENDPOINTS = ${JSON.stringify(endpoints)};
|
|
117
117
|
|
|
118
118
|
let currentEp = '';
|
|
119
119
|
|
|
120
120
|
// Setup host variables contextually on layout initialization
|
|
121
121
|
document.getElementById('base-url').value = window.location.origin;
|
|
122
122
|
|
|
123
|
-
function getBase() { return document.getElementById('base-url').value.replace(/\/$/, ''); }
|
|
124
|
-
function getJwt() { return document.getElementById('jwt-input').value.trim(); }
|
|
125
|
-
|
|
126
123
|
function buildSidebar() {
|
|
127
124
|
const sidebar = document.getElementById('sidebar-nav');
|
|
128
125
|
const keys = Object.keys(ENDPOINTS);
|
|
@@ -135,9 +132,9 @@ function buildSidebar() {
|
|
|
135
132
|
keys.forEach((key, index) => {
|
|
136
133
|
const ep = ENDPOINTS[key];
|
|
137
134
|
const div = document.createElement('div');
|
|
138
|
-
div.className =
|
|
135
|
+
div.className = index === 0 ? 'nav-item active' : 'nav-item';
|
|
139
136
|
div.setAttribute('data-ep', key);
|
|
140
|
-
div.innerHTML =
|
|
137
|
+
div.innerHTML = '<span class="method-badge ' + ep.method + '">' + ep.method + '</span><span class="nav-label">' + ep.path + '</span>';
|
|
141
138
|
|
|
142
139
|
div.addEventListener('click', () => {
|
|
143
140
|
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
@@ -159,41 +156,39 @@ function renderPanel(epKey) {
|
|
|
159
156
|
const main = document.getElementById('main-panel');
|
|
160
157
|
if(!ep) return;
|
|
161
158
|
|
|
162
|
-
let html =
|
|
163
|
-
<div class="endpoint-
|
|
164
|
-
|
|
165
|
-
<span
|
|
166
|
-
|
|
167
|
-
</div>
|
|
168
|
-
<div class="endpoint-desc">\${ep.desc}</div>
|
|
169
|
-
\`;
|
|
159
|
+
let html = '<div class="endpoint-title">' + ep.title + '</div>' +
|
|
160
|
+
'<div class="endpoint-path">' +
|
|
161
|
+
'<span class="method-badge ' + ep.method + '">' + ep.method + '</span>' +
|
|
162
|
+
'<span>' + ep.path + '</span>' +
|
|
163
|
+
'</div>' +
|
|
164
|
+
'<div class="endpoint-desc">' + ep.desc + '</div>';
|
|
170
165
|
|
|
171
|
-
if (ep.params
|
|
172
|
-
html +=
|
|
166
|
+
if (ep.params && ep.params.length) {
|
|
167
|
+
html += '<div class="form-section"><div class="form-section-title">Path Parameters</div>';
|
|
173
168
|
ep.params.forEach(p => {
|
|
174
|
-
html +=
|
|
175
|
-
<label class="field-label"
|
|
176
|
-
<input type="text" id="param
|
|
177
|
-
</div
|
|
169
|
+
html += '<div class="field-row">' +
|
|
170
|
+
'<label class="field-label">' + p.label + '</label>' +
|
|
171
|
+
'<input type="text" id="param-' + p.name + '" placeholder="' + p.placeholder + '" />' +
|
|
172
|
+
'</div>';
|
|
178
173
|
});
|
|
179
|
-
html +=
|
|
174
|
+
html += '</div>';
|
|
180
175
|
}
|
|
181
176
|
|
|
182
|
-
if (ep.fields
|
|
183
|
-
html +=
|
|
177
|
+
if (ep.fields && ep.fields.length) {
|
|
178
|
+
html += '<div class="form-section"><div class="form-section-title">JSON Request Body Raw Payload</div>';
|
|
184
179
|
ep.fields.forEach(f => {
|
|
185
|
-
html +=
|
|
186
|
-
<label class="field-label"
|
|
187
|
-
<input type="text" id="field
|
|
188
|
-
</div
|
|
180
|
+
html += '<div class="field-row">' +
|
|
181
|
+
'<label class="field-label">' + f.label + '</label>' +
|
|
182
|
+
'<input type="text" id="field-' + f.name + '" value=\'{"key": "value"}\' />' +
|
|
183
|
+
'</div>';
|
|
189
184
|
});
|
|
190
|
-
html +=
|
|
185
|
+
html += '</div>';
|
|
191
186
|
}
|
|
192
187
|
|
|
193
|
-
html +=
|
|
194
|
-
<button class="btn" onclick="sendRequest()">Execute Route</button>
|
|
195
|
-
<button class="btn btn-secondary" onclick="clearResponse()">Clear Context</button>
|
|
196
|
-
</div
|
|
188
|
+
html += '<div class="btn-row">' +
|
|
189
|
+
'<button class="btn" onclick="sendRequest()">Execute Route</button>' +
|
|
190
|
+
'<button class="btn btn-secondary" onclick="clearResponse()">Clear Context</button>' +
|
|
191
|
+
'</div>';
|
|
197
192
|
|
|
198
193
|
main.innerHTML = html;
|
|
199
194
|
}
|
|
@@ -204,9 +199,9 @@ async function sendRequest() {
|
|
|
204
199
|
|
|
205
200
|
if (ep.params) {
|
|
206
201
|
for (const p of ep.params) {
|
|
207
|
-
const val = document.getElementById(
|
|
208
|
-
if (!val) { showToast(
|
|
209
|
-
path = path.replace(
|
|
202
|
+
const val = document.getElementById('param-' + p.name)?.value.trim();
|
|
203
|
+
if (!val) { showToast('⚠ Parameter ' + p.label + ' required'); return; }
|
|
204
|
+
path = path.replace(':' + p.name, encodeURIComponent(val));
|
|
210
205
|
}
|
|
211
206
|
}
|
|
212
207
|
|
|
@@ -214,11 +209,11 @@ async function sendRequest() {
|
|
|
214
209
|
const headers = { 'Content-Type': 'application/json' };
|
|
215
210
|
|
|
216
211
|
const jwt = getJwt();
|
|
217
|
-
if (jwt) headers['Authorization'] =
|
|
212
|
+
if (jwt) headers['Authorization'] = 'Bearer ' + jwt;
|
|
218
213
|
|
|
219
214
|
let body = undefined;
|
|
220
|
-
if (ep.fields
|
|
221
|
-
const rawVal = document.getElementById(
|
|
215
|
+
if (ep.fields && ep.fields.length && ['POST','PUT','PATCH'].includes(ep.method)) {
|
|
216
|
+
const rawVal = document.getElementById('field-' + ep.fields[0].name).value.trim();
|
|
222
217
|
try {
|
|
223
218
|
JSON.parse(rawVal);
|
|
224
219
|
body = rawVal;
|
|
@@ -254,8 +249,8 @@ function setResponse(data, state, status, ms) {
|
|
|
254
249
|
return;
|
|
255
250
|
}
|
|
256
251
|
|
|
257
|
-
badge.className =
|
|
258
|
-
badge.textContent =
|
|
252
|
+
badge.className = 'status-badge ' + (state === 'ok' ? 'status-ok' : 'status-err');
|
|
253
|
+
badge.textContent = status + ' · ' + ms + 'ms';
|
|
259
254
|
body.innerHTML = highlightJson(typeof data === 'string' ? data : JSON.stringify(data, null, 2));
|
|
260
255
|
}
|
|
261
256
|
|
|
@@ -273,10 +268,10 @@ function highlightJson(str) {
|
|
|
273
268
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
274
269
|
.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, match => {
|
|
275
270
|
if (/^"/.test(match)) {
|
|
276
|
-
if (/:$/.test(match)) return
|
|
277
|
-
return
|
|
271
|
+
if (/:$/.test(match)) return '<span class="json-key">' + match + '</span>';
|
|
272
|
+
return '<span class="json-str">' + match + '</span>';
|
|
278
273
|
}
|
|
279
|
-
return
|
|
274
|
+
return '<span class="json-num">' + match + '</span>';
|
|
280
275
|
});
|
|
281
276
|
}
|
|
282
277
|
|
|
@@ -286,7 +281,7 @@ function showToast(msg) {
|
|
|
286
281
|
setTimeout(() => t.classList.remove('show'), 2500);
|
|
287
282
|
}
|
|
288
283
|
|
|
289
|
-
// Initial
|
|
284
|
+
// Initial script activation
|
|
290
285
|
buildSidebar();
|
|
291
286
|
</script>
|
|
292
287
|
</body>
|
package/index.js
CHANGED
|
@@ -1,83 +1,68 @@
|
|
|
1
1
|
import { getHtmlTemplate } from './htmlTemplate.js';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Express middleware to automatically map routes and serve the endtester UI
|
|
5
|
-
*/
|
|
6
3
|
export function endtesterExpress() {
|
|
7
4
|
return (req, res, next) => {
|
|
8
|
-
// We only intercept requests hitting the tester path
|
|
9
5
|
if (req.path !== '/api/tester' && req.path !== '/api/tester/') {
|
|
10
6
|
return next();
|
|
11
7
|
}
|
|
12
8
|
|
|
13
|
-
// 1. Inspect the Express app to extract all registered routes
|
|
14
9
|
const expressApp = req.app;
|
|
15
|
-
const routesStack = expressApp._router.stack;
|
|
16
10
|
const detectedEndpoints = {};
|
|
17
11
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
.replace(
|
|
27
|
-
.replace('\\', '');
|
|
12
|
+
// Recursive function to dig through all layers of Express routes
|
|
13
|
+
function parseStack(stack, prefix = '') {
|
|
14
|
+
if (!stack) return;
|
|
15
|
+
|
|
16
|
+
stack.forEach((layer) => {
|
|
17
|
+
if (layer.route) {
|
|
18
|
+
// It's a direct route (e.g., app.get('/path'))
|
|
19
|
+
const methods = Object.keys(layer.route.methods);
|
|
20
|
+
const path = (prefix + layer.route.path).replace(/\/+/g, '/');
|
|
28
21
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
if (path.includes('/api/tester')) return; // Skip ourselves
|
|
23
|
+
|
|
24
|
+
methods.forEach((method) => {
|
|
25
|
+
const httpMethod = method.toUpperCase();
|
|
26
|
+
const key = `${httpMethod.toLowerCase()}-${path.replace(/[^a-zA-Z0-9]/g, '-')}`;
|
|
27
|
+
|
|
28
|
+
const pathParams = layer.route.keys ? layer.route.keys.map(k => ({
|
|
29
|
+
name: k.name,
|
|
30
|
+
label: k.name.toUpperCase(),
|
|
31
|
+
placeholder: 'value'
|
|
32
|
+
})) : [];
|
|
33
|
+
|
|
34
|
+
detectedEndpoints[key] = {
|
|
35
|
+
method: httpMethod,
|
|
36
|
+
path: path,
|
|
37
|
+
auth: false,
|
|
38
|
+
title: `${httpMethod} ${path}`,
|
|
39
|
+
desc: `Auto-discovered endpoint: ${path}`,
|
|
40
|
+
params: pathParams,
|
|
41
|
+
fields: ['POST', 'PUT', 'PATCH'].includes(httpMethod) ? [{ name: 'payload', label: 'JSON Body', type: 'text' }] : []
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
} else if (layer.name === 'router' && layer.handle && layer.handle.stack) {
|
|
45
|
+
// It's a router middleware (e.g., app.use('/api', myRouter))
|
|
46
|
+
let routerPath = '';
|
|
47
|
+
if (layer.regexp) {
|
|
48
|
+
// Extract the base path string from the router regex safely
|
|
49
|
+
const match = layer.regexp.toString().match(/^\/\^\\(.*?)\\\/\?/);
|
|
50
|
+
if (match && match[1]) {
|
|
51
|
+
routerPath = match[1].replace(/\\/g, '');
|
|
52
|
+
}
|
|
32
53
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
54
|
+
parseStack(layer.handle.stack, prefix + '/' + routerPath);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
36
58
|
|
|
37
|
-
//
|
|
38
|
-
|
|
59
|
+
// Fire the scanner on the main app router stack
|
|
60
|
+
if (expressApp._router && expressApp._router.stack) {
|
|
61
|
+
parseStack(expressApp._router.stack);
|
|
62
|
+
}
|
|
39
63
|
|
|
40
|
-
|
|
64
|
+
const fullHtml = getHtmlTemplate(detectedEndpoints);
|
|
41
65
|
res.setHeader('Content-Type', 'text/html');
|
|
42
66
|
return res.send(fullHtml);
|
|
43
67
|
};
|
|
44
68
|
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Formats Express route data into the shape your UI script expects
|
|
48
|
-
*/
|
|
49
|
-
function processRoute(route, targetObject, prefix = '') {
|
|
50
|
-
const fullPath = `${prefix}${route.path}`.replace(/\/+/g, '/');
|
|
51
|
-
|
|
52
|
-
// Skip the tester endpoint itself to avoid recursion confusion
|
|
53
|
-
if (fullPath.includes('/api/tester')) return;
|
|
54
|
-
|
|
55
|
-
// Extract path parameters (e.g., :id)
|
|
56
|
-
const pathParams = route.keys ? route.keys.map(k => ({
|
|
57
|
-
name: k.name,
|
|
58
|
-
label: k.name.toUpperCase(),
|
|
59
|
-
placeholder: 'value'
|
|
60
|
-
})) : [];
|
|
61
|
-
|
|
62
|
-
Object.keys(route.methods).forEach((method) => {
|
|
63
|
-
if (!route.methods[method]) return;
|
|
64
|
-
|
|
65
|
-
const httpMethod = method.toUpperCase();
|
|
66
|
-
// Unique key for your UI state
|
|
67
|
-
const key = `${httpMethod.toLowerCase()}-${fullPath.replace(/[^a-zA-Z0-9]/g, '-')}`;
|
|
68
|
-
|
|
69
|
-
// Infer basic structure based on request types
|
|
70
|
-
const needsBody = ['POST', 'PUT', 'PATCH'].includes(httpMethod);
|
|
71
|
-
|
|
72
|
-
targetObject[key] = {
|
|
73
|
-
method: httpMethod,
|
|
74
|
-
path: fullPath,
|
|
75
|
-
auth: true, // Default to true, users can toggle or override
|
|
76
|
-
title: `${httpMethod} ${fullPath}`,
|
|
77
|
-
desc: `Auto-generated tester for ${fullPath}`,
|
|
78
|
-
params: pathParams,
|
|
79
|
-
queryParams: httpMethod === 'GET' ? [{ name: 'limit', label: 'Limit', placeholder: '10' }] : [],
|
|
80
|
-
fields: needsBody ? [{ name: 'payload', label: 'JSON Body Field', type: 'text', placeholder: 'Data string' }] : []
|
|
81
|
-
};
|
|
82
|
-
});
|
|
83
|
-
}
|