@aimeloic/monkey-tester 2.0.5 → 2.0.7
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 +249 -270
- package/index.js +3 -244
- package/monkey.js +198 -0
- package/package.json +1 -1
package/htmlTemplate.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function getHtmlTemplate(endpoints) {
|
|
2
4
|
const safeJsonString = Buffer.from(JSON.stringify(endpoints)).toString('base64');
|
|
3
5
|
|
|
4
|
-
return
|
|
5
|
-
<!DOCTYPE html>
|
|
6
|
+
return `<!DOCTYPE html>
|
|
6
7
|
<html lang="en">
|
|
7
8
|
<head>
|
|
8
9
|
<meta charset="UTF-8">
|
|
9
10
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
10
|
-
<title>
|
|
11
|
+
<title>Monkey Tester — API Sandbox</title>
|
|
11
12
|
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
|
|
12
13
|
<style>
|
|
13
14
|
:root {
|
|
@@ -24,167 +25,143 @@ export function getHtmlTemplate(endpoints) {
|
|
|
24
25
|
--blue: #5a86c0;
|
|
25
26
|
--radius: 8px;
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
+
|
|
28
29
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
29
|
-
|
|
30
|
+
|
|
30
31
|
body {
|
|
31
|
-
background: var(--bg);
|
|
32
|
-
color: var(--text);
|
|
33
|
-
font-family: 'DM Sans', sans-serif;
|
|
34
|
-
font-size: 14px;
|
|
32
|
+
background: var(--bg);
|
|
33
|
+
color: var(--text);
|
|
34
|
+
font-family: 'DM Sans', sans-serif;
|
|
35
|
+
font-size: 14px;
|
|
35
36
|
height: 100vh;
|
|
36
|
-
overflow: hidden;
|
|
37
|
+
overflow: hidden;
|
|
37
38
|
background-image: radial-gradient(ellipse 80% 60% at 50% -20%, #3a2a0a22 0%, transparent 70%);
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
+
|
|
40
41
|
header {
|
|
41
|
-
border-bottom: 1px solid var(--border);
|
|
42
|
-
padding: 16px 32px;
|
|
43
|
-
display: flex;
|
|
44
|
-
align-items: center;
|
|
42
|
+
border-bottom: 1px solid var(--border);
|
|
43
|
+
padding: 16px 32px;
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
45
46
|
gap: 20px;
|
|
46
|
-
background: #0e0c09ee;
|
|
47
|
-
backdrop-filter: blur(8px);
|
|
47
|
+
background: #0e0c09ee;
|
|
48
|
+
backdrop-filter: blur(8px);
|
|
48
49
|
height: 65px;
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
+
|
|
51
52
|
.logo { font-family: 'Playfair Display', serif; font-size: 20px; color: var(--accent); letter-spacing: 0.02em; }
|
|
52
53
|
.logo span { color: var(--text-dim); font-size: 11px; font-family: 'DM Mono', monospace; display: inline-block; margin-left: 8px; font-weight: 400; }
|
|
53
54
|
.header-right { margin-left: auto; display: flex; align-items: center; gap: 16px; }
|
|
54
55
|
.jwt-wrap, .base-url-wrap { display: flex; align-items: center; gap: 8px; }
|
|
55
56
|
.jwt-wrap label, .base-url-wrap label { color: var(--text-dim); font-size: 11px; font-family: 'DM Mono', monospace; letter-spacing: 0.05em; }
|
|
56
|
-
|
|
57
|
+
|
|
57
58
|
#jwt-input, #base-url {
|
|
58
59
|
background: var(--surface2); border: 1px solid var(--border); color: var(--text);
|
|
59
|
-
font-family: 'DM Mono', monospace; font-size: 12px; padding: 6px 12px;
|
|
60
|
+
font-family: 'DM Mono', monospace; font-size: 12px; padding: 6px 12px;
|
|
61
|
+
border-radius: var(--radius); width: 220px; outline: none;
|
|
60
62
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
height: calc(100vh - 65px);
|
|
63
|
+
|
|
64
|
+
.layout {
|
|
65
|
+
display: grid;
|
|
66
|
+
grid-template-columns: 280px 1fr 450px;
|
|
67
|
+
height: calc(100vh - 65px);
|
|
67
68
|
overflow: hidden;
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
-
aside {
|
|
71
|
-
border-right: 1px solid var(--border);
|
|
72
|
-
overflow-y: auto;
|
|
73
|
-
padding: 16px 0;
|
|
70
|
+
|
|
71
|
+
aside {
|
|
72
|
+
border-right: 1px solid var(--border);
|
|
73
|
+
overflow-y: auto;
|
|
74
|
+
padding: 16px 0;
|
|
74
75
|
background: #0b0907;
|
|
75
76
|
}
|
|
76
|
-
|
|
77
|
+
|
|
77
78
|
.section-label { font-size: 10px; font-family: 'DM Mono', monospace; color: var(--text-dim); letter-spacing: 0.12em; text-transform: uppercase; padding: 12px 18px 6px; }
|
|
78
|
-
|
|
79
|
+
|
|
79
80
|
.nav-item { display: flex; align-items: center; gap: 10px; padding: 10px 18px; cursor: pointer; border-left: 2px solid transparent; color: var(--text-dim); transition: all 0.2s; }
|
|
80
81
|
.nav-item:hover { background: var(--surface); color: var(--text); }
|
|
81
82
|
.nav-item.active { border-left-color: var(--accent); background: var(--surface); color: var(--accent); }
|
|
82
|
-
|
|
83
|
+
|
|
83
84
|
.method-badge { font-family: 'DM Mono', monospace; font-size: 9px; font-weight: 600; padding: 2px 6px; border-radius: 4px; min-width: 52px; text-align: center; text-transform: uppercase; }
|
|
84
|
-
.GET
|
|
85
|
-
.POST
|
|
86
|
-
.PUT
|
|
85
|
+
.GET { background: #1a3a22; color: #6ba05a; }
|
|
86
|
+
.POST { background: #1a2e3a; color: #5a86c0; }
|
|
87
|
+
.PUT { background: #3a2e10; color: #e8a838; }
|
|
88
|
+
.PATCH { background: #3a2e10; color: #e8a838; }
|
|
87
89
|
.DELETE { background: #3a1a14; color: #d45c3c; }
|
|
88
|
-
|
|
90
|
+
|
|
89
91
|
.nav-label { font-size: 12px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
90
|
-
|
|
91
|
-
main {
|
|
92
|
-
overflow-y: auto;
|
|
93
|
-
padding: 32px;
|
|
92
|
+
|
|
93
|
+
main {
|
|
94
|
+
overflow-y: auto;
|
|
95
|
+
padding: 32px;
|
|
94
96
|
background: #0e0c09;
|
|
95
97
|
}
|
|
96
|
-
|
|
98
|
+
|
|
97
99
|
.endpoint-title { font-family: 'Playfair Display', serif; font-size: 24px; color: var(--accent); margin-bottom: 8px; }
|
|
98
|
-
.endpoint-path
|
|
99
|
-
.endpoint-desc
|
|
100
|
-
|
|
100
|
+
.endpoint-path { font-family: 'DM Mono', monospace; font-size: 13px; color: var(--text-dim); margin-bottom: 24px; display: flex; align-items: center; gap: 8px; }
|
|
101
|
+
.endpoint-desc { color: var(--text-dim); font-size: 13px; line-height: 1.6; margin-bottom: 24px; border-left: 2px solid var(--border); padding-left: 12px; }
|
|
102
|
+
|
|
101
103
|
.form-section { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 20px; }
|
|
102
104
|
.form-section-title { font-size: 11px; font-family: 'DM Mono', monospace; color: var(--text-dim); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 16px; border-bottom: 1px solid var(--border); padding-bottom: 6px; }
|
|
103
|
-
|
|
105
|
+
|
|
104
106
|
.field-row { display: grid; grid-template-columns: 150px 1fr; align-items: center; gap: 16px; margin-bottom: 14px; }
|
|
105
107
|
.field-row:last-child { margin-bottom: 0; }
|
|
106
108
|
.field-label { font-family: 'DM Mono', monospace; font-size: 12px; color: var(--text-dim); text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
107
|
-
|
|
108
|
-
input[type=text], input[type=password], input[type=number], input[type=date],
|
|
109
|
-
|
|
109
|
+
|
|
110
|
+
input[type=text], input[type=password], input[type=number], input[type=date],
|
|
111
|
+
input[type=tel], input[type=url], input[type=email], select {
|
|
112
|
+
background: var(--surface2); border: 1px solid var(--border); color: var(--text);
|
|
113
|
+
font-family: 'DM Sans', sans-serif; font-size: 13px; padding: 8px 12px;
|
|
114
|
+
border-radius: var(--radius); width: 100%; outline: none; transition: border-color 0.2s;
|
|
110
115
|
}
|
|
111
116
|
input:focus { border-color: var(--accent); }
|
|
112
|
-
|
|
117
|
+
|
|
113
118
|
.btn-row { margin-top: 24px; display: flex; gap: 12px; }
|
|
114
119
|
.btn { background: var(--accent); color: #0e0c09; border: none; padding: 10px 24px; border-radius: var(--radius); font-size: 13px; font-weight: 500; cursor: pointer; transition: background-color 0.2s; }
|
|
115
120
|
.btn:hover { background: #f0b850; }
|
|
116
121
|
.btn-secondary { background: var(--surface2); color: var(--text-dim); border: 1px solid var(--border); }
|
|
117
122
|
.btn-secondary:hover { color: var(--text); background: var(--surface); }
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
.response-panel {
|
|
121
|
-
border-left: 1px solid var(--border);
|
|
122
|
-
display: flex;
|
|
123
|
-
flex-direction: column;
|
|
124
|
-
overflow: hidden;
|
|
125
|
-
background: #110e0a;
|
|
126
|
-
}
|
|
123
|
+
|
|
124
|
+
.response-panel { border-left: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; background: #110e0a; }
|
|
127
125
|
.response-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; background: var(--surface); height: 50px; }
|
|
128
126
|
.response-header-title { font-size: 11px; font-family: 'DM Mono', monospace; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
129
127
|
.status-badge { font-family: 'DM Mono', monospace; font-size: 12px; margin-left: auto; padding: 2px 8px; border-radius: 4px; font-weight: 500; }
|
|
130
|
-
.status-ok
|
|
131
|
-
.status-err
|
|
128
|
+
.status-ok { background: #1a3a22; color: #6ba05a; }
|
|
129
|
+
.status-err { background: #3a1a14; color: #d45c3c; }
|
|
132
130
|
.status-idle { background: var(--surface2); color: var(--text-dim); }
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
.response-body {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
.response-body.empty {
|
|
143
|
-
color: var(--text-dim);
|
|
144
|
-
display: flex;
|
|
145
|
-
align-items: center;
|
|
146
|
-
justify-content: center;
|
|
147
|
-
padding: 20px;
|
|
148
|
-
text-align: center;
|
|
149
|
-
font-size: 13px;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/* FIXED: Token code output container handles micro horizontal data flows elegantly */
|
|
153
|
-
.json-render-block {
|
|
154
|
-
display: block;
|
|
155
|
-
padding: 20px;
|
|
156
|
-
margin: 0;
|
|
157
|
-
font-family: 'DM Mono', monospace;
|
|
158
|
-
font-size: 12px;
|
|
159
|
-
line-height: 1.5;
|
|
160
|
-
white-space: pre;
|
|
161
|
-
overflow-x: auto;
|
|
162
|
-
word-break: normal;
|
|
163
|
-
word-wrap: normal;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
.json-key { color: #e8a838; }
|
|
167
|
-
.json-str { color: #9ab878; }
|
|
131
|
+
|
|
132
|
+
.response-body { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 0; background: #0d0b08; }
|
|
133
|
+
.response-body.empty { color: var(--text-dim); display: flex; align-items: center; justify-content: center; padding: 20px; text-align: center; font-size: 13px; }
|
|
134
|
+
|
|
135
|
+
.json-render-block { display: block; padding: 20px; margin: 0; font-family: 'DM Mono', monospace; font-size: 12px; line-height: 1.5; white-space: pre; overflow-x: auto; word-break: normal; word-wrap: normal; }
|
|
136
|
+
|
|
137
|
+
.json-key { color: #e8a838; }
|
|
138
|
+
.json-str { color: #9ab878; }
|
|
168
139
|
.json-num { color: #5a86c0; }
|
|
169
|
-
|
|
140
|
+
.json-bool { color: #c47a1e; }
|
|
141
|
+
.json-null { color: var(--text-dim); }
|
|
142
|
+
|
|
170
143
|
#toast { position: fixed; bottom: 24px; right: 24px; background: var(--surface2); border: 1px solid var(--border); padding: 10px 18px; border-radius: var(--radius); opacity: 0; transition: all .25s; z-index: 1000; font-family: 'DM Mono', monospace; font-size: 12px; color: var(--accent); }
|
|
171
144
|
#toast.show { opacity: 1; }
|
|
145
|
+
|
|
146
|
+
.empty-state { text-align: center; padding: 60px 20px; color: var(--text-dim); }
|
|
147
|
+
.empty-state .monkey { font-size: 48px; margin-bottom: 16px; }
|
|
148
|
+
.empty-state h2 { color: var(--text); font-family: 'Playfair Display', serif; margin-bottom: 8px; }
|
|
172
149
|
</style>
|
|
173
150
|
</head>
|
|
174
151
|
<body>
|
|
175
152
|
|
|
176
|
-
<div id="
|
|
153
|
+
<div id="__monkey_data__" data-payload="${safeJsonString}" style="display:none;"></div>
|
|
177
154
|
|
|
178
155
|
<header>
|
|
179
|
-
<div class="logo"
|
|
156
|
+
<div class="logo">🐒 Monkey Tester <span>API Sandbox</span></div>
|
|
180
157
|
<div class="header-right">
|
|
181
158
|
<div class="base-url-wrap">
|
|
182
159
|
<label>TARGET HOST</label>
|
|
183
160
|
<input id="base-url" type="text" value="">
|
|
184
161
|
</div>
|
|
185
162
|
<div class="jwt-wrap">
|
|
186
|
-
<label>BEARER
|
|
187
|
-
<input id="jwt-input" type="text" placeholder="
|
|
163
|
+
<label>BEARER TOKEN</label>
|
|
164
|
+
<input id="jwt-input" type="text" placeholder="Paste token here...">
|
|
188
165
|
</div>
|
|
189
166
|
</div>
|
|
190
167
|
</header>
|
|
@@ -196,210 +173,212 @@ export function getHtmlTemplate(endpoints) {
|
|
|
196
173
|
<main id="main-panel"></main>
|
|
197
174
|
<div class="response-panel">
|
|
198
175
|
<div class="response-header">
|
|
199
|
-
<span class="response-header-title">Response
|
|
176
|
+
<span class="response-header-title">Response</span>
|
|
200
177
|
<span id="status-badge" class="status-badge status-idle">—</span>
|
|
201
178
|
</div>
|
|
202
|
-
<div class="response-body empty" id="response-body">Execute a request
|
|
179
|
+
<div class="response-body empty" id="response-body">Execute a request to see the response</div>
|
|
203
180
|
</div>
|
|
204
181
|
</div>
|
|
205
182
|
|
|
206
183
|
<div id="toast"></div>
|
|
207
184
|
|
|
208
185
|
<script>
|
|
209
|
-
const
|
|
210
|
-
|
|
186
|
+
const ENDPOINTS = JSON.parse(atob(document.getElementById('__monkey_data__').getAttribute('data-payload')));
|
|
187
|
+
let currentKey = null;
|
|
211
188
|
|
|
212
|
-
|
|
189
|
+
document.getElementById('base-url').value = window.location.origin;
|
|
213
190
|
|
|
214
|
-
|
|
191
|
+
// ── Sidebar ────────────────────────────────────────────────────────────────
|
|
192
|
+
function buildSidebar() {
|
|
193
|
+
const sidebar = document.getElementById('sidebar-nav');
|
|
194
|
+
const keys = Object.keys(ENDPOINTS);
|
|
215
195
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
196
|
+
if (keys.length === 0) {
|
|
197
|
+
sidebar.innerHTML += '<div style="padding:18px;color:var(--text-dim);font-size:12px">No endpoints discovered.</div>';
|
|
198
|
+
showEmptyState();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
219
201
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
202
|
+
keys.forEach((key, i) => {
|
|
203
|
+
const ep = ENDPOINTS[key];
|
|
204
|
+
const item = document.createElement('div');
|
|
205
|
+
item.className = 'nav-item' + (i === 0 ? ' active' : '');
|
|
206
|
+
item.setAttribute('data-key', key);
|
|
207
|
+
item.innerHTML =
|
|
208
|
+
'<span class="method-badge ' + ep.method + '">' + ep.method + '</span>' +
|
|
209
|
+
'<span class="nav-label">' + ep.path + '</span>';
|
|
210
|
+
item.addEventListener('click', () => {
|
|
211
|
+
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
212
|
+
item.classList.add('active');
|
|
213
|
+
clearResponse();
|
|
214
|
+
renderPanel(key);
|
|
215
|
+
});
|
|
216
|
+
sidebar.appendChild(item);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
renderPanel(keys[0]);
|
|
223
220
|
}
|
|
224
221
|
|
|
225
|
-
|
|
222
|
+
// ── Panel ──────────────────────────────────────────────────────────────────
|
|
223
|
+
function renderPanel(key) {
|
|
224
|
+
currentKey = key;
|
|
226
225
|
const ep = ENDPOINTS[key];
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
226
|
+
const main = document.getElementById('main-panel');
|
|
227
|
+
if (!ep) return;
|
|
228
|
+
|
|
229
|
+
let html =
|
|
230
|
+
'<div class="endpoint-title">' + ep.title + '</div>' +
|
|
231
|
+
'<div class="endpoint-path"><span class="method-badge ' + ep.method + '">' + ep.method + '</span>' +
|
|
232
|
+
'<span>' + ep.path + '</span></div>' +
|
|
233
|
+
'<div class="endpoint-desc">' + ep.desc + '</div>';
|
|
234
|
+
|
|
235
|
+
// Path params
|
|
236
|
+
if (ep.params && ep.params.length) {
|
|
237
|
+
html += '<div class="form-section"><div class="form-section-title">Path Parameters</div>';
|
|
238
|
+
ep.params.forEach(function(p) {
|
|
239
|
+
html +=
|
|
240
|
+
'<div class="field-row">' +
|
|
241
|
+
'<label class="field-label">' + p.label + '</label>' +
|
|
242
|
+
'<input type="text" id="param-' + p.name + '" placeholder="' + p.placeholder + '" />' +
|
|
243
|
+
'</div>';
|
|
244
|
+
});
|
|
245
|
+
html += '</div>';
|
|
246
|
+
}
|
|
239
247
|
|
|
240
|
-
|
|
241
|
-
|
|
248
|
+
// Body fields
|
|
249
|
+
if (ep.fields && ep.fields.length) {
|
|
250
|
+
html += '<div class="form-section"><div class="form-section-title">Request Body</div>';
|
|
251
|
+
ep.fields.forEach(function(f) {
|
|
252
|
+
html +=
|
|
253
|
+
'<div class="field-row">' +
|
|
254
|
+
'<label class="field-label">' + f.label + '</label>' +
|
|
255
|
+
'<input type="' + (f.type || 'text') + '" id="field-' + f.name + '" placeholder="' + (f.placeholder || '') + '" />' +
|
|
256
|
+
'</div>';
|
|
257
|
+
});
|
|
258
|
+
html += '</div>';
|
|
259
|
+
}
|
|
242
260
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
261
|
+
html +=
|
|
262
|
+
'<div class="btn-row">' +
|
|
263
|
+
'<button class="btn" onclick="sendRequest()">► Send</button>' +
|
|
264
|
+
'<button class="btn btn-secondary" onclick="clearResponse()">Clear</button>' +
|
|
265
|
+
'</div>';
|
|
247
266
|
|
|
248
|
-
|
|
249
|
-
currentEp = epKey;
|
|
250
|
-
const ep = ENDPOINTS[epKey];
|
|
251
|
-
const main = document.getElementById('main-panel');
|
|
252
|
-
if (!ep) return;
|
|
253
|
-
|
|
254
|
-
let html = \`
|
|
255
|
-
<div class="endpoint-title">\${ep.title}</div>
|
|
256
|
-
<div class="endpoint-path">
|
|
257
|
-
<span class="method-badge \${ep.method}">\${ep.method}</span>
|
|
258
|
-
<span>\${ep.path}</span>
|
|
259
|
-
</div>
|
|
260
|
-
<div class="endpoint-desc">\${ep.desc}</div>
|
|
261
|
-
\`;
|
|
262
|
-
|
|
263
|
-
if (ep.params && ep.params.length) {
|
|
264
|
-
html += \`<div class="form-section"><div class="form-section-title">Path Parameters</div>\`;
|
|
265
|
-
ep.params.forEach(function(p) {
|
|
266
|
-
const inputHtml = makeInputString('text', 'param-' + p.name, p.placeholder);
|
|
267
|
-
html += \`
|
|
268
|
-
<div class="field-row">
|
|
269
|
-
<label class="field-label">\${p.label}</label>
|
|
270
|
-
\${inputHtml}
|
|
271
|
-
</div>
|
|
272
|
-
\`;
|
|
273
|
-
});
|
|
274
|
-
html += \`</div>\`;
|
|
267
|
+
main.innerHTML = html;
|
|
275
268
|
}
|
|
276
269
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
<div class="field-row">
|
|
283
|
-
<label class="field-label">\${f.label}</label>
|
|
284
|
-
\${inputHtml}
|
|
285
|
-
</div>
|
|
286
|
-
\`;
|
|
287
|
-
});
|
|
288
|
-
html += \`</div>\`;
|
|
270
|
+
function showEmptyState() {
|
|
271
|
+
document.getElementById('main-panel').innerHTML =
|
|
272
|
+
'<div class="empty-state"><div class="monkey">🐒</div>' +
|
|
273
|
+
'<h2>No endpoints found</h2>' +
|
|
274
|
+
'<p>Make sure <code>app.use(endtesterExpress())</code> is added after your routes.</p></div>';
|
|
289
275
|
}
|
|
290
276
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
</div>
|
|
296
|
-
\`;
|
|
277
|
+
// ── Request ────────────────────────────────────────────────────────────────
|
|
278
|
+
async function sendRequest() {
|
|
279
|
+
const ep = ENDPOINTS[currentKey];
|
|
280
|
+
let path = ep.path;
|
|
297
281
|
|
|
298
|
-
|
|
299
|
-
|
|
282
|
+
if (ep.params && ep.params.length) {
|
|
283
|
+
for (const p of ep.params) {
|
|
284
|
+
const val = (document.getElementById('param-' + p.name) || {}).value || '';
|
|
285
|
+
if (!val.trim()) { showToast('⚠ Path param "' + p.label + '" is required'); return; }
|
|
286
|
+
path = path.replace(':' + p.name, encodeURIComponent(val.trim()));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
300
289
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
290
|
+
const baseUrl = document.getElementById('base-url').value.replace(/\/+$/, '');
|
|
291
|
+
const url = baseUrl + path;
|
|
292
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
293
|
+
const jwt = document.getElementById('jwt-input').value.trim();
|
|
294
|
+
if (jwt) headers['Authorization'] = 'Bearer ' + jwt;
|
|
295
|
+
|
|
296
|
+
let body = undefined;
|
|
297
|
+
if (['POST', 'PUT', 'PATCH'].includes(ep.method) && ep.fields && ep.fields.length) {
|
|
298
|
+
const payload = {};
|
|
299
|
+
let hasData = false;
|
|
300
|
+
|
|
301
|
+
ep.fields.forEach(function(f) {
|
|
302
|
+
const el = document.getElementById('field-' + f.name);
|
|
303
|
+
if (!el) return;
|
|
304
|
+
let v = el.value.trim();
|
|
305
|
+
if (v === '') return; // Avoid submitting empty strings for unfilled fields
|
|
306
|
+
|
|
307
|
+
if (f.type === 'number') v = Number(v);
|
|
308
|
+
payload[f.name] = v;
|
|
309
|
+
hasData = true;
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
if (hasData) body = JSON.stringify(payload);
|
|
313
|
+
}
|
|
304
314
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
315
|
+
setResponse(null, 'loading');
|
|
316
|
+
const t0 = Date.now();
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
const res = await fetch(url, { method: ep.method, headers, body });
|
|
320
|
+
const ms = Date.now() - t0;
|
|
321
|
+
const text = await res.text();
|
|
322
|
+
let data;
|
|
323
|
+
try { data = JSON.parse(text); } catch { data = text; }
|
|
324
|
+
setResponse(data, res.ok ? 'ok' : 'err', res.status, ms);
|
|
325
|
+
} catch (err) {
|
|
326
|
+
setResponse({ error: err.message }, 'err', 'FAIL', 0);
|
|
310
327
|
}
|
|
311
328
|
}
|
|
312
329
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const targetEl = document.getElementById('field-' + f.name);
|
|
325
|
-
if (targetEl) {
|
|
326
|
-
let entryVal = targetEl.value.trim();
|
|
327
|
-
if (f.type === 'number' && entryVal !== '') {
|
|
328
|
-
entryVal = Number(entryVal);
|
|
329
|
-
}
|
|
330
|
-
jsonPayload[f.name] = entryVal;
|
|
331
|
-
}
|
|
330
|
+
// ── Response ───────────────────────────────────────────────────────────────
|
|
331
|
+
function setResponse(data, state, status, ms) {
|
|
332
|
+
const badge = document.getElementById('status-badge');
|
|
333
|
+
const body = document.getElementById('response-body');
|
|
334
|
+
|
|
335
|
+
if (state === 'loading') {
|
|
336
|
+
badge.className = 'status-badge status-idle';
|
|
337
|
+
badge.textContent = '…';
|
|
338
|
+
body.className = 'response-body empty';
|
|
339
|
+
body.textContent = 'Sending…';
|
|
340
|
+
return;
|
|
332
341
|
}
|
|
333
|
-
body = JSON.stringify(jsonPayload);
|
|
334
|
-
}
|
|
335
342
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const ms = Date.now() - start;
|
|
342
|
-
const text = await res.text();
|
|
343
|
-
let json;
|
|
344
|
-
try { json = JSON.parse(text); } catch { json = text; }
|
|
345
|
-
setResponse(json, res.ok ? 'ok' : 'err', res.status, ms);
|
|
346
|
-
} catch (err) {
|
|
347
|
-
setResponse({ error: err.message }, 'err', 'FAIL', 0);
|
|
343
|
+
badge.className = 'status-badge ' + (state === 'ok' ? 'status-ok' : 'status-err');
|
|
344
|
+
badge.textContent = status + ' · ' + ms + 'ms';
|
|
345
|
+
body.className = 'response-body';
|
|
346
|
+
const str = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
|
347
|
+
body.innerHTML = '<pre class="json-render-block">' + highlight(str) + '</pre>';
|
|
348
348
|
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function setResponse(data, state, status, ms) {
|
|
352
|
-
const badge = document.getElementById('status-badge');
|
|
353
|
-
const body = document.getElementById('response-body');
|
|
354
349
|
|
|
355
|
-
|
|
356
|
-
badge.className = 'status-badge status-idle';
|
|
357
|
-
badge.textContent = '
|
|
350
|
+
function clearResponse() {
|
|
351
|
+
document.getElementById('status-badge').className = 'status-badge status-idle';
|
|
352
|
+
document.getElementById('status-badge').textContent = '—';
|
|
353
|
+
const body = document.getElementById('response-body');
|
|
358
354
|
body.className = 'response-body empty';
|
|
359
|
-
body.
|
|
360
|
-
return;
|
|
355
|
+
body.textContent = 'Execute a request to see the response';
|
|
361
356
|
}
|
|
362
357
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
badge.className = 'status-badge status-idle';
|
|
376
|
-
badge.textContent = '—';
|
|
377
|
-
body.className = 'response-body empty';
|
|
378
|
-
body.textContent = 'Execute a request row to generate feedback data';
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function highlightJson(str) {
|
|
382
|
-
return str
|
|
383
|
-
.replace(/&/g, '&').replace(/[<]/g, '<').replace(/[>]/g, '>')
|
|
384
|
-
.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function(match) {
|
|
385
|
-
if (/^"/.test(match)) {
|
|
386
|
-
if (/:$/.test(match)) return '<span class="json-key">' + match + '</span>';
|
|
387
|
-
return '<span class="json-str">' + match + '</span>';
|
|
388
|
-
}
|
|
389
|
-
return '<span class="json-num">' + match + '</span>';
|
|
390
|
-
});
|
|
391
|
-
}
|
|
358
|
+
function highlight(str) {
|
|
359
|
+
return str
|
|
360
|
+
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
|
361
|
+
.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false)\b|\bnull\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function(m) {
|
|
362
|
+
if (/^"/.test(m)) return /:$/.test(m)
|
|
363
|
+
? '<span class="json-key">' + m + '</span>'
|
|
364
|
+
: '<span class="json-str">' + m + '</span>';
|
|
365
|
+
if (/true|false/.test(m)) return '<span class="json-bool">' + m + '</span>';
|
|
366
|
+
if (/null/.test(m)) return '<span class="json-null">' + m + '</span>';
|
|
367
|
+
return '<span class="json-num">' + m + '</span>';
|
|
368
|
+
});
|
|
369
|
+
}
|
|
392
370
|
|
|
393
|
-
function showToast(msg) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
371
|
+
function showToast(msg) {
|
|
372
|
+
const t = document.getElementById('toast');
|
|
373
|
+
t.textContent = msg;
|
|
374
|
+
t.classList.add('show');
|
|
375
|
+
setTimeout(() => t.classList.remove('show'), 2500);
|
|
376
|
+
}
|
|
399
377
|
|
|
400
|
-
buildSidebar();
|
|
378
|
+
buildSidebar();
|
|
401
379
|
</script>
|
|
402
380
|
</body>
|
|
403
|
-
</html
|
|
404
|
-
`;
|
|
381
|
+
</html>`;
|
|
405
382
|
}
|
|
383
|
+
|
|
384
|
+
module.exports = { getHtmlTemplate };
|
package/index.js
CHANGED
|
@@ -1,246 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
return (req, res, next) => {
|
|
5
|
-
if (
|
|
6
|
-
req.path !== '/api/tester' &&
|
|
7
|
-
req.path !== '/api/tester/'
|
|
8
|
-
) {
|
|
9
|
-
return next();
|
|
10
|
-
}
|
|
3
|
+
const { endtesterExpress } = require('./monkey.js');
|
|
11
4
|
|
|
12
|
-
|
|
13
|
-
const detectedEndpoints = {};
|
|
14
|
-
|
|
15
|
-
// =========================
|
|
16
|
-
// Detect Input Types
|
|
17
|
-
// =========================
|
|
18
|
-
function detectInputType(field) {
|
|
19
|
-
const lower = field.toLowerCase();
|
|
20
|
-
|
|
21
|
-
if (lower.includes('email')) return 'email';
|
|
22
|
-
if (lower.includes('password')) return 'password';
|
|
23
|
-
if (lower.includes('date')) return 'date';
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
lower.includes('age') ||
|
|
27
|
-
lower.includes('price') ||
|
|
28
|
-
lower.includes('salary') ||
|
|
29
|
-
lower.includes('stock') ||
|
|
30
|
-
lower.includes('quantity') ||
|
|
31
|
-
lower.includes('count') ||
|
|
32
|
-
lower.includes('total') ||
|
|
33
|
-
lower.includes('amount') ||
|
|
34
|
-
lower.includes('id')
|
|
35
|
-
) {
|
|
36
|
-
return 'number';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
lower.includes('phone') ||
|
|
41
|
-
lower.includes('tel')
|
|
42
|
-
) {
|
|
43
|
-
return 'tel';
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
lower.includes('url') ||
|
|
48
|
-
lower.includes('website')
|
|
49
|
-
) {
|
|
50
|
-
return 'url';
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return 'text';
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// =========================
|
|
57
|
-
// Context-Aware Static Fallbacks
|
|
58
|
-
// =========================
|
|
59
|
-
function getFallbackFieldsForPath(path) {
|
|
60
|
-
const lowerPath = path.toLowerCase();
|
|
61
|
-
let rawFields = [];
|
|
62
|
-
|
|
63
|
-
if (lowerPath.includes('login') || lowerPath.includes('auth') || lowerPath.includes('signin')) {
|
|
64
|
-
rawFields = ['email', 'password'];
|
|
65
|
-
} else if (lowerPath.includes('user') || lowerPath.includes('register') || lowerPath.includes('signup')) {
|
|
66
|
-
rawFields = ['username', 'email', 'password'];
|
|
67
|
-
} else if (lowerPath.includes('product')) {
|
|
68
|
-
rawFields = ['name', 'price', 'stock'];
|
|
69
|
-
} else {
|
|
70
|
-
return [];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return rawFields.map(field => ({
|
|
74
|
-
name: field,
|
|
75
|
-
label: field.charAt(0).toUpperCase() + field.slice(1),
|
|
76
|
-
type: detectInputType(field),
|
|
77
|
-
placeholder: `Enter ${field}`
|
|
78
|
-
}));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// =========================
|
|
82
|
-
// Extract req.body fields via Source Inspection
|
|
83
|
-
// =========================
|
|
84
|
-
function extractBodyFields(handler) {
|
|
85
|
-
try {
|
|
86
|
-
const source = handler.toString();
|
|
87
|
-
|
|
88
|
-
// If it's a bound handler or lacks source reference code text
|
|
89
|
-
if (!source || source.includes('[native code]')) return [];
|
|
90
|
-
|
|
91
|
-
const regex = /(const|let|var)\s*\{\s*([^}]+)\s*\}\s*=\s*req\.body/gs;
|
|
92
|
-
const matches = [...source.matchAll(regex)];
|
|
93
|
-
const fields = [];
|
|
94
|
-
|
|
95
|
-
matches.forEach((match) => {
|
|
96
|
-
const cleanedVariablesBlock = match[2]
|
|
97
|
-
.replace(/\/\/.*$/gm, '')
|
|
98
|
-
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
99
|
-
.replace(/[\r\n\t]/g, ' ');
|
|
100
|
-
|
|
101
|
-
const variables = cleanedVariablesBlock
|
|
102
|
-
.split(',')
|
|
103
|
-
.map(v => v.trim())
|
|
104
|
-
.filter(Boolean);
|
|
105
|
-
|
|
106
|
-
variables.forEach((field) => {
|
|
107
|
-
let realField = field;
|
|
108
|
-
|
|
109
|
-
if (field.includes(':')) {
|
|
110
|
-
realField = field.split(':')[0].trim();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (realField.includes('=')) {
|
|
114
|
-
realField = realField.split('=')[0].trim();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
realField = realField.trim();
|
|
118
|
-
|
|
119
|
-
const alreadyExists = fields.find(f => f.name === realField);
|
|
120
|
-
|
|
121
|
-
if (!alreadyExists && realField) {
|
|
122
|
-
fields.push({
|
|
123
|
-
name: realField,
|
|
124
|
-
label: realField.charAt(0).toUpperCase() + realField.slice(1),
|
|
125
|
-
type: detectInputType(realField),
|
|
126
|
-
placeholder: `Enter ${realField}`
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
return fields;
|
|
133
|
-
} catch (err) {
|
|
134
|
-
console.error('Field extraction error:', err);
|
|
135
|
-
return [];
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// =========================
|
|
140
|
-
// Parse Express Stack
|
|
141
|
-
// =========================
|
|
142
|
-
function parseStack(stack, prefix = '') {
|
|
143
|
-
if (!stack) return;
|
|
144
|
-
|
|
145
|
-
stack.forEach((layer) => {
|
|
146
|
-
// =========================
|
|
147
|
-
// ROUTES
|
|
148
|
-
// =========================
|
|
149
|
-
if (layer.route) {
|
|
150
|
-
const methods = Object.keys(layer.route.methods);
|
|
151
|
-
const path = (prefix + layer.route.path).replace(/\/+/g, '/');
|
|
152
|
-
|
|
153
|
-
if (path.includes('/api/tester')) {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
methods.forEach((method) => {
|
|
158
|
-
const httpMethod = method.toUpperCase();
|
|
159
|
-
const key = `${httpMethod.toLowerCase()}-` + path.replace(/[^a-zA-Z0-9]/g, '-');
|
|
160
|
-
|
|
161
|
-
const pathParams = layer.route.keys
|
|
162
|
-
? layer.route.keys.map((k) => ({
|
|
163
|
-
name: k.name,
|
|
164
|
-
label: k.name.toUpperCase(),
|
|
165
|
-
placeholder: 'value'
|
|
166
|
-
}))
|
|
167
|
-
: [];
|
|
168
|
-
|
|
169
|
-
// =========================
|
|
170
|
-
// BODY FIELDS COMPILING
|
|
171
|
-
// =========================
|
|
172
|
-
let bodyFields = [];
|
|
173
|
-
|
|
174
|
-
if (['POST', 'PUT', 'PATCH'].includes(httpMethod)) {
|
|
175
|
-
layer.route.stack.forEach((stackLayer) => {
|
|
176
|
-
if (stackLayer.handle && typeof stackLayer.handle === 'function') {
|
|
177
|
-
const extractedFields = extractBodyFields(stackLayer.handle);
|
|
178
|
-
bodyFields.push(...extractedFields);
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// Deduplicate discovered elements
|
|
183
|
-
bodyFields = bodyFields.filter(
|
|
184
|
-
(field, index, self) => index === self.findIndex(f => f.name === field.name)
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
// CRITICAL FAILSAFE: If code reflection extracted nothing, apply smart path-based fallback fields
|
|
188
|
-
if (bodyFields.length === 0) {
|
|
189
|
-
bodyFields = getFallbackFieldsForPath(path);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
detectedEndpoints[key] = {
|
|
194
|
-
method: httpMethod,
|
|
195
|
-
path,
|
|
196
|
-
title: `${httpMethod} ${path}`,
|
|
197
|
-
desc: `Auto-discovered endpoint: ${path}`,
|
|
198
|
-
params: pathParams,
|
|
199
|
-
fields: bodyFields
|
|
200
|
-
};
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// =========================
|
|
205
|
-
// NESTED ROUTERS
|
|
206
|
-
// =========================
|
|
207
|
-
else if (
|
|
208
|
-
layer.name === 'router' &&
|
|
209
|
-
layer.handle &&
|
|
210
|
-
layer.handle.stack
|
|
211
|
-
) {
|
|
212
|
-
let routerPath = '';
|
|
213
|
-
|
|
214
|
-
if (layer.regexp) {
|
|
215
|
-
const match = layer.regexp
|
|
216
|
-
.toString()
|
|
217
|
-
.match(/^\/\^\\(.*?)\\\/\?/);
|
|
218
|
-
|
|
219
|
-
if (match && match[1]) {
|
|
220
|
-
routerPath = match[1].replace(/\\/g, '');
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
parseStack(
|
|
225
|
-
layer.handle.stack,
|
|
226
|
-
prefix + '/' + routerPath
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// =========================
|
|
233
|
-
// START PARSING
|
|
234
|
-
// =========================
|
|
235
|
-
if (expressApp._router && expressApp._router.stack) {
|
|
236
|
-
parseStack(expressApp._router.stack);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// =========================
|
|
240
|
-
// RENDER HTML
|
|
241
|
-
// =========================
|
|
242
|
-
const fullHtml = getHtmlTemplate(detectedEndpoints);
|
|
243
|
-
res.setHeader('Content-Type', 'text/html');
|
|
244
|
-
return res.send(fullHtml);
|
|
245
|
-
};
|
|
246
|
-
}
|
|
5
|
+
module.exports = { endtesterExpress };
|
package/monkey.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getHtmlTemplate } = require('./htmlTemplate');
|
|
4
|
+
|
|
5
|
+
// ─── Field type inference ─────────────────────────────────────────────────────
|
|
6
|
+
function inferType(name) {
|
|
7
|
+
const n = name.toLowerCase();
|
|
8
|
+
if (n.includes('email')) return 'email';
|
|
9
|
+
if (n.includes('password') || n.includes('pass')) return 'password';
|
|
10
|
+
if (n.includes('date') || n.includes('birth')) return 'date';
|
|
11
|
+
if (n.includes('phone') || n.includes('tel')) return 'tel';
|
|
12
|
+
if (n.includes('url') || n.includes('website') || n.includes('link')) return 'url';
|
|
13
|
+
if (
|
|
14
|
+
n.includes('age') || n.includes('price') || n.includes('amount') ||
|
|
15
|
+
n.includes('count') || n.includes('qty') || n.includes('quantity') ||
|
|
16
|
+
n.includes('stock') || n.includes('salary') || n.includes('total') ||
|
|
17
|
+
(n === 'id') || n.endsWith('_id') || n.endsWith('Id')
|
|
18
|
+
) return 'number';
|
|
19
|
+
return 'text';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildField(name) {
|
|
23
|
+
return {
|
|
24
|
+
name,
|
|
25
|
+
label: name.charAt(0).toUpperCase() + name.slice(1).replace(/([A-Z])/g, ' $1'),
|
|
26
|
+
type: inferType(name),
|
|
27
|
+
placeholder: `Enter ${name}`
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Extract req.body fields from handler source ──────────────────────────────
|
|
32
|
+
function extractBodyFields(handler) {
|
|
33
|
+
try {
|
|
34
|
+
const source = handler.toString();
|
|
35
|
+
if (!source || source.includes('[native code]')) return [];
|
|
36
|
+
|
|
37
|
+
const seen = new Map();
|
|
38
|
+
|
|
39
|
+
// Pattern 1 — destructuring: const { email, password } = req.body
|
|
40
|
+
const destructRe = /(?:const|let|var)\s*\{\s*([^}]+)\s*\}\s*=\s*req\.body/g;
|
|
41
|
+
let m;
|
|
42
|
+
while ((m = destructRe.exec(source)) !== null) {
|
|
43
|
+
m[1].split(',').forEach(part => {
|
|
44
|
+
const name = part.split(':')[0].split('=')[0].trim();
|
|
45
|
+
if (name && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) && !seen.has(name)) {
|
|
46
|
+
seen.set(name, buildField(name));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Pattern 2 — property access: req.body.email / req.body['email']
|
|
52
|
+
const accessRe = /req\.body\.([a-zA-Z_$][a-zA-Z0-9_$]*)|req\.body\[['"]([a-zA-Z_$][a-zA-Z0-9_$]*)['"]]/g;
|
|
53
|
+
while ((m = accessRe.exec(source)) !== null) {
|
|
54
|
+
const name = m[1] || m[2];
|
|
55
|
+
if (name && !seen.has(name)) seen.set(name, buildField(name));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return Array.from(seen.values());
|
|
59
|
+
} catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Path-based fallback fields ───────────────────────────────────────────────
|
|
65
|
+
function fallbackFields(path) {
|
|
66
|
+
const p = path.toLowerCase();
|
|
67
|
+
|
|
68
|
+
if (p.includes('login') || p.includes('signin') || p.includes('auth/login')) {
|
|
69
|
+
return ['email', 'password'].map(buildField);
|
|
70
|
+
}
|
|
71
|
+
if (p.includes('register') || p.includes('signup') || p.includes('auth/register')) {
|
|
72
|
+
return ['username', 'email', 'password'].map(buildField);
|
|
73
|
+
}
|
|
74
|
+
if (p.includes('user')) {
|
|
75
|
+
return ['username', 'email', 'password'].map(buildField);
|
|
76
|
+
}
|
|
77
|
+
if (p.includes('product')) {
|
|
78
|
+
return ['name', 'price', 'stock'].map(buildField);
|
|
79
|
+
}
|
|
80
|
+
if (p.includes('order')) {
|
|
81
|
+
return ['productId', 'quantity', 'address'].map(buildField);
|
|
82
|
+
}
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Extract router prefix safely from Express layer ─────────────────────────
|
|
87
|
+
function extractRouterPrefix(layer) {
|
|
88
|
+
if (!layer.regexp) return '';
|
|
89
|
+
if (layer.path) return layer.path; // If explicit paths exist
|
|
90
|
+
|
|
91
|
+
let src = layer.regexp.source;
|
|
92
|
+
|
|
93
|
+
// Strip out boilerplate generated patterns from default express mounting structures
|
|
94
|
+
src = src
|
|
95
|
+
.replace('^\\/', '/')
|
|
96
|
+
.replace('\\/?(?=\\/|$)', '')
|
|
97
|
+
.replace('(?:\\/(?=$))?(?=\\/|$)', '')
|
|
98
|
+
.replace('\\/', '/');
|
|
99
|
+
|
|
100
|
+
const cleanPath = src.split('(?')[0].replace(/\\/g, '');
|
|
101
|
+
return cleanPath === '/' ? '' : cleanPath;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── Walk the Express router stack recursively ────────────────────────────────
|
|
105
|
+
function parseStack(stack, detectedEndpoints, prefix = '') {
|
|
106
|
+
if (!Array.isArray(stack)) return;
|
|
107
|
+
|
|
108
|
+
for (const layer of stack) {
|
|
109
|
+
// ── Named route (app.get / app.post …) ──────────────────────────────────
|
|
110
|
+
if (layer.route) {
|
|
111
|
+
const rawPath = typeof layer.route.path === 'string'
|
|
112
|
+
? layer.route.path
|
|
113
|
+
: (layer.route.path ? String(layer.route.path) : '');
|
|
114
|
+
|
|
115
|
+
const fullPath = (prefix + rawPath).replace(/\/+/g, '/') || '/';
|
|
116
|
+
|
|
117
|
+
if (fullPath.startsWith('/api/tester')) continue;
|
|
118
|
+
|
|
119
|
+
const methods = Object.keys(layer.route.methods || {});
|
|
120
|
+
|
|
121
|
+
for (const method of methods) {
|
|
122
|
+
const httpMethod = method.toUpperCase();
|
|
123
|
+
const key = `${httpMethod}::${fullPath}`;
|
|
124
|
+
|
|
125
|
+
// ── Path params (:id, :slug …) safely parsed using matchAll ─────────
|
|
126
|
+
const pathParams = [];
|
|
127
|
+
const paramRe = /:([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
|
|
128
|
+
const matches = [...fullPath.matchAll(paramRe)];
|
|
129
|
+
|
|
130
|
+
for (const pm of matches) {
|
|
131
|
+
pathParams.push({
|
|
132
|
+
name: pm[1],
|
|
133
|
+
label: pm[1].charAt(0).toUpperCase() + pm[1].slice(1),
|
|
134
|
+
placeholder: 'value'
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Body fields ────────────────────────────────────────────────────
|
|
139
|
+
let bodyFields = [];
|
|
140
|
+
if (['POST', 'PUT', 'PATCH'].includes(httpMethod)) {
|
|
141
|
+
const handlers = (layer.route.stack || []).map(sl => sl.handle).filter(Boolean);
|
|
142
|
+
for (const handler of handlers) {
|
|
143
|
+
bodyFields.push(...extractBodyFields(handler));
|
|
144
|
+
}
|
|
145
|
+
const seen = new Map();
|
|
146
|
+
bodyFields = bodyFields.filter(f => {
|
|
147
|
+
if (seen.has(f.name)) return false;
|
|
148
|
+
seen.set(f.name, true);
|
|
149
|
+
return true;
|
|
150
|
+
});
|
|
151
|
+
if (bodyFields.length === 0) {
|
|
152
|
+
bodyFields = fallbackFields(fullPath);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
detectedEndpoints[key] = {
|
|
157
|
+
method: httpMethod,
|
|
158
|
+
path: fullPath,
|
|
159
|
+
title: `${httpMethod} ${fullPath}`,
|
|
160
|
+
desc: `Auto-discovered endpoint — ${fullPath}`,
|
|
161
|
+
params: pathParams,
|
|
162
|
+
fields: bodyFields,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Nested router (app.use('/prefix', router)) ───────────────────────────
|
|
168
|
+
else if (layer.name === 'router' && layer.handle && layer.handle.stack) {
|
|
169
|
+
const routerPrefix = extractRouterPrefix(layer);
|
|
170
|
+
parseStack(layer.handle.stack, detectedEndpoints, prefix + routerPrefix);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── Middleware ───────────────────────────────────────────────────────────────
|
|
176
|
+
function endtesterExpress() {
|
|
177
|
+
return function monkeyTesterMiddleware(req, res, next) {
|
|
178
|
+
if (req.path !== '/api/tester' && req.path !== '/api/tester/') {
|
|
179
|
+
return next();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const app = req.app;
|
|
183
|
+
const detectedEndpoints = {};
|
|
184
|
+
|
|
185
|
+
const rootStack =
|
|
186
|
+
(app._router && app._router.stack) || // Express 4
|
|
187
|
+
(app.router && app.router.stack) || // Express 5
|
|
188
|
+
[];
|
|
189
|
+
|
|
190
|
+
parseStack(rootStack, detectedEndpoints);
|
|
191
|
+
|
|
192
|
+
const html = getHtmlTemplate(detectedEndpoints);
|
|
193
|
+
res.setHeader('Content-Type', 'text/html');
|
|
194
|
+
return res.send(html);
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = { endtesterExpress };
|