@aimeloic/monkey-tester 2.0.4 → 2.0.6
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 +243 -263
- package/index.js +3 -244
- package/monkey.js +204 -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,163 +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
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
|
-
.layout {
|
|
63
|
-
display: grid;
|
|
64
|
-
grid-template-columns: 280px 1fr 450px;
|
|
65
|
-
height: calc(100vh - 65px);
|
|
63
|
+
|
|
64
|
+
.layout {
|
|
65
|
+
display: grid;
|
|
66
|
+
grid-template-columns: 280px 1fr 450px;
|
|
67
|
+
height: calc(100vh - 65px);
|
|
66
68
|
overflow: hidden;
|
|
67
69
|
}
|
|
68
|
-
|
|
69
|
-
aside {
|
|
70
|
-
border-right: 1px solid var(--border);
|
|
71
|
-
overflow-y: auto;
|
|
72
|
-
padding: 16px 0;
|
|
70
|
+
|
|
71
|
+
aside {
|
|
72
|
+
border-right: 1px solid var(--border);
|
|
73
|
+
overflow-y: auto;
|
|
74
|
+
padding: 16px 0;
|
|
73
75
|
background: #0b0907;
|
|
74
76
|
}
|
|
75
|
-
|
|
77
|
+
|
|
76
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; }
|
|
77
|
-
|
|
79
|
+
|
|
78
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; }
|
|
79
81
|
.nav-item:hover { background: var(--surface); color: var(--text); }
|
|
80
82
|
.nav-item.active { border-left-color: var(--accent); background: var(--surface); color: var(--accent); }
|
|
81
|
-
|
|
83
|
+
|
|
82
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; }
|
|
83
|
-
.GET
|
|
84
|
-
.POST
|
|
85
|
-
.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; }
|
|
86
89
|
.DELETE { background: #3a1a14; color: #d45c3c; }
|
|
87
|
-
|
|
90
|
+
|
|
88
91
|
.nav-label { font-size: 12px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
89
|
-
|
|
90
|
-
main {
|
|
91
|
-
overflow-y: auto;
|
|
92
|
-
padding: 32px;
|
|
92
|
+
|
|
93
|
+
main {
|
|
94
|
+
overflow-y: auto;
|
|
95
|
+
padding: 32px;
|
|
93
96
|
background: #0e0c09;
|
|
94
97
|
}
|
|
95
|
-
|
|
98
|
+
|
|
96
99
|
.endpoint-title { font-family: 'Playfair Display', serif; font-size: 24px; color: var(--accent); margin-bottom: 8px; }
|
|
97
|
-
.endpoint-path
|
|
98
|
-
.endpoint-desc
|
|
99
|
-
|
|
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
|
+
|
|
100
103
|
.form-section { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 20px; }
|
|
101
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; }
|
|
102
|
-
|
|
105
|
+
|
|
103
106
|
.field-row { display: grid; grid-template-columns: 150px 1fr; align-items: center; gap: 16px; margin-bottom: 14px; }
|
|
104
107
|
.field-row:last-child { margin-bottom: 0; }
|
|
105
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; }
|
|
106
|
-
|
|
107
|
-
input[type=text], input[type=password], input[type=number], input[type=date],
|
|
108
|
-
|
|
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;
|
|
109
115
|
}
|
|
110
116
|
input:focus { border-color: var(--accent); }
|
|
111
|
-
|
|
117
|
+
|
|
112
118
|
.btn-row { margin-top: 24px; display: flex; gap: 12px; }
|
|
113
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; }
|
|
114
120
|
.btn:hover { background: #f0b850; }
|
|
115
121
|
.btn-secondary { background: var(--surface2); color: var(--text-dim); border: 1px solid var(--border); }
|
|
116
122
|
.btn-secondary:hover { color: var(--text); background: var(--surface); }
|
|
117
|
-
|
|
118
|
-
.response-panel {
|
|
119
|
-
border-left: 1px solid var(--border);
|
|
120
|
-
display: flex;
|
|
121
|
-
flex-direction: column;
|
|
122
|
-
overflow: hidden;
|
|
123
|
-
background: #110e0a;
|
|
124
|
-
}
|
|
123
|
+
|
|
124
|
+
.response-panel { border-left: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; background: #110e0a; }
|
|
125
125
|
.response-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; background: var(--surface); height: 50px; }
|
|
126
126
|
.response-header-title { font-size: 11px; font-family: 'DM Mono', monospace; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
127
127
|
.status-badge { font-family: 'DM Mono', monospace; font-size: 12px; margin-left: auto; padding: 2px 8px; border-radius: 4px; font-weight: 500; }
|
|
128
|
-
.status-ok
|
|
129
|
-
.status-err
|
|
128
|
+
.status-ok { background: #1a3a22; color: #6ba05a; }
|
|
129
|
+
.status-err { background: #3a1a14; color: #d45c3c; }
|
|
130
130
|
.status-idle { background: var(--surface2); color: var(--text-dim); }
|
|
131
|
-
|
|
132
|
-
.response-body {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
.response-body.empty {
|
|
140
|
-
color: var(--text-dim);
|
|
141
|
-
display: flex;
|
|
142
|
-
align-items: center;
|
|
143
|
-
justify-content: center;
|
|
144
|
-
padding: 20px;
|
|
145
|
-
text-align: center;
|
|
146
|
-
font-size: 13px;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
.json-render-block {
|
|
150
|
-
display: block;
|
|
151
|
-
padding: 20px;
|
|
152
|
-
margin: 0;
|
|
153
|
-
font-family: 'DM Mono', monospace;
|
|
154
|
-
font-size: 12px;
|
|
155
|
-
line-height: 1.5;
|
|
156
|
-
white-space: pre;
|
|
157
|
-
overflow-x: auto;
|
|
158
|
-
word-break: normal;
|
|
159
|
-
word-wrap: normal;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
.json-key { color: #e8a838; }
|
|
163
|
-
.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; }
|
|
164
139
|
.json-num { color: #5a86c0; }
|
|
165
|
-
|
|
140
|
+
.json-bool { color: #c47a1e; }
|
|
141
|
+
.json-null { color: var(--text-dim); }
|
|
142
|
+
|
|
166
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); }
|
|
167
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; }
|
|
168
149
|
</style>
|
|
169
150
|
</head>
|
|
170
151
|
<body>
|
|
171
152
|
|
|
172
|
-
<div id="
|
|
153
|
+
<div id="__monkey_data__" data-payload="${safeJsonString}" style="display:none;"></div>
|
|
173
154
|
|
|
174
155
|
<header>
|
|
175
|
-
<div class="logo"
|
|
156
|
+
<div class="logo">🐒 Monkey Tester <span>API Sandbox</span></div>
|
|
176
157
|
<div class="header-right">
|
|
177
158
|
<div class="base-url-wrap">
|
|
178
159
|
<label>TARGET HOST</label>
|
|
179
160
|
<input id="base-url" type="text" value="">
|
|
180
161
|
</div>
|
|
181
162
|
<div class="jwt-wrap">
|
|
182
|
-
<label>BEARER
|
|
183
|
-
<input id="jwt-input" type="text" placeholder="
|
|
163
|
+
<label>BEARER TOKEN</label>
|
|
164
|
+
<input id="jwt-input" type="text" placeholder="Paste token here...">
|
|
184
165
|
</div>
|
|
185
166
|
</div>
|
|
186
167
|
</header>
|
|
@@ -192,207 +173,206 @@ export function getHtmlTemplate(endpoints) {
|
|
|
192
173
|
<main id="main-panel"></main>
|
|
193
174
|
<div class="response-panel">
|
|
194
175
|
<div class="response-header">
|
|
195
|
-
<span class="response-header-title">Response
|
|
176
|
+
<span class="response-header-title">Response</span>
|
|
196
177
|
<span id="status-badge" class="status-badge status-idle">—</span>
|
|
197
178
|
</div>
|
|
198
|
-
<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>
|
|
199
180
|
</div>
|
|
200
181
|
</div>
|
|
201
182
|
|
|
202
183
|
<div id="toast"></div>
|
|
203
184
|
|
|
204
185
|
<script>
|
|
205
|
-
const
|
|
206
|
-
|
|
186
|
+
const ENDPOINTS = JSON.parse(atob(document.getElementById('__monkey_data__').getAttribute('data-payload')));
|
|
187
|
+
let currentKey = null;
|
|
188
|
+
|
|
189
|
+
document.getElementById('base-url').value = window.location.origin;
|
|
207
190
|
|
|
208
|
-
|
|
191
|
+
// ── Sidebar ────────────────────────────────────────────────────────────────
|
|
192
|
+
function buildSidebar() {
|
|
193
|
+
const sidebar = document.getElementById('sidebar-nav');
|
|
194
|
+
const keys = Object.keys(ENDPOINTS);
|
|
209
195
|
|
|
210
|
-
|
|
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
|
+
}
|
|
211
201
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
+
});
|
|
215
218
|
|
|
216
|
-
|
|
217
|
-
sidebar.innerHTML += '<div style="padding:15px; color:var(--text-dim)">No active application endpoints discovered.</div>';
|
|
218
|
-
return;
|
|
219
|
+
renderPanel(keys[0]);
|
|
219
220
|
}
|
|
220
221
|
|
|
221
|
-
|
|
222
|
+
// ── Panel ──────────────────────────────────────────────────────────────────
|
|
223
|
+
function renderPanel(key) {
|
|
224
|
+
currentKey = key;
|
|
222
225
|
const ep = ENDPOINTS[key];
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
+
}
|
|
235
247
|
|
|
236
|
-
|
|
237
|
-
|
|
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
|
+
}
|
|
238
260
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
let html = \`
|
|
247
|
-
<div class="endpoint-title">\\\${ep.title}</div>
|
|
248
|
-
<div class="endpoint-path">
|
|
249
|
-
<span class="method-badge \\\${ep.method}">\\\${ep.method}</span>
|
|
250
|
-
<span>\\\${ep.path}</span>
|
|
251
|
-
</div>
|
|
252
|
-
<div class="endpoint-desc">\\\${ep.desc}</div>
|
|
253
|
-
\`;
|
|
254
|
-
|
|
255
|
-
if (ep.params && ep.params.length) {
|
|
256
|
-
html += \`<div class="form-section"><div class="form-section-title">Path Parameters</div>\`;
|
|
257
|
-
ep.params.forEach(function(p) {
|
|
258
|
-
const ph = p.placeholder ? ' placeholder="' + p.placeholder + '"' : '';
|
|
259
|
-
html += \`
|
|
260
|
-
<div class="field-row">
|
|
261
|
-
<label class="field-label">\\\${p.label}</label>
|
|
262
|
-
<input type="text" id="param-\\\${p.name}" \${ph} />
|
|
263
|
-
</div>
|
|
264
|
-
\`;
|
|
265
|
-
});
|
|
266
|
-
html += \`</div>\`;
|
|
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>';
|
|
266
|
+
|
|
267
|
+
main.innerHTML = html;
|
|
267
268
|
}
|
|
268
269
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
html += \`
|
|
275
|
-
<div class="field-row">
|
|
276
|
-
<label class="field-label">\\\${f.label}</label>
|
|
277
|
-
<input type="\${type}" id="field-\\\${f.name}" \${ph} />
|
|
278
|
-
</div>
|
|
279
|
-
\`;
|
|
280
|
-
});
|
|
281
|
-
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>';
|
|
282
275
|
}
|
|
283
276
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
</div>
|
|
289
|
-
\`;
|
|
277
|
+
// ── Request ────────────────────────────────────────────────────────────────
|
|
278
|
+
async function sendRequest() {
|
|
279
|
+
const ep = ENDPOINTS[currentKey];
|
|
280
|
+
let path = ep.path;
|
|
290
281
|
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
}
|
|
293
289
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
+
ep.fields.forEach(function(f) {
|
|
300
|
+
const el = document.getElementById('field-' + f.name);
|
|
301
|
+
if (!el) return;
|
|
302
|
+
let v = el.value.trim();
|
|
303
|
+
if (f.type === 'number' && v !== '') v = Number(v);
|
|
304
|
+
payload[f.name] = v;
|
|
305
|
+
});
|
|
306
|
+
body = JSON.stringify(payload);
|
|
307
|
+
}
|
|
297
308
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
309
|
+
setResponse(null, 'loading');
|
|
310
|
+
const t0 = Date.now();
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
const res = await fetch(url, { method: ep.method, headers, body });
|
|
314
|
+
const ms = Date.now() - t0;
|
|
315
|
+
const text = await res.text();
|
|
316
|
+
let data;
|
|
317
|
+
try { data = JSON.parse(text); } catch { data = text; }
|
|
318
|
+
setResponse(data, res.ok ? 'ok' : 'err', res.status, ms);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
setResponse({ error: err.message }, 'err', 'FAIL', 0);
|
|
303
321
|
}
|
|
304
322
|
}
|
|
305
323
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const targetEl = document.getElementById('field-' + f.name);
|
|
318
|
-
if (targetEl) {
|
|
319
|
-
let entryVal = targetEl.value.trim();
|
|
320
|
-
if (f.type === 'number' && entryVal !== '') {
|
|
321
|
-
entryVal = Number(entryVal);
|
|
322
|
-
}
|
|
323
|
-
jsonPayload[f.name] = entryVal;
|
|
324
|
-
}
|
|
324
|
+
// ── Response ───────────────────────────────────────────────────────────────
|
|
325
|
+
function setResponse(data, state, status, ms) {
|
|
326
|
+
const badge = document.getElementById('status-badge');
|
|
327
|
+
const body = document.getElementById('response-body');
|
|
328
|
+
|
|
329
|
+
if (state === 'loading') {
|
|
330
|
+
badge.className = 'status-badge status-idle';
|
|
331
|
+
badge.textContent = '…';
|
|
332
|
+
body.className = 'response-body empty';
|
|
333
|
+
body.textContent = 'Sending…';
|
|
334
|
+
return;
|
|
325
335
|
}
|
|
326
|
-
body = JSON.stringify(jsonPayload);
|
|
327
|
-
}
|
|
328
336
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const ms = Date.now() - start;
|
|
335
|
-
const text = await res.text();
|
|
336
|
-
let json;
|
|
337
|
-
try { json = JSON.parse(text); } catch { json = text; }
|
|
338
|
-
setResponse(json, res.ok ? 'ok' : 'err', res.status, ms);
|
|
339
|
-
} catch (err) {
|
|
340
|
-
setResponse({ error: err.message }, 'err', 'FAIL', 0);
|
|
337
|
+
badge.className = 'status-badge ' + (state === 'ok' ? 'status-ok' : 'status-err');
|
|
338
|
+
badge.textContent = status + ' · ' + ms + 'ms';
|
|
339
|
+
body.className = 'response-body';
|
|
340
|
+
const str = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
|
341
|
+
body.innerHTML = '<pre class="json-render-block">' + highlight(str) + '</pre>';
|
|
341
342
|
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function setResponse(data, state, status, ms) {
|
|
345
|
-
const badge = document.getElementById('status-badge');
|
|
346
|
-
const body = document.getElementById('response-body');
|
|
347
343
|
|
|
348
|
-
|
|
349
|
-
badge.className = 'status-badge status-idle';
|
|
350
|
-
badge.textContent = '
|
|
344
|
+
function clearResponse() {
|
|
345
|
+
document.getElementById('status-badge').className = 'status-badge status-idle';
|
|
346
|
+
document.getElementById('status-badge').textContent = '—';
|
|
347
|
+
const body = document.getElementById('response-body');
|
|
351
348
|
body.className = 'response-body empty';
|
|
352
|
-
body.
|
|
353
|
-
return;
|
|
349
|
+
body.textContent = 'Execute a request to see the response';
|
|
354
350
|
}
|
|
355
351
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
badge.textContent = '—';
|
|
369
|
-
body.className = 'response-body empty';
|
|
370
|
-
body.textContent = 'Execute a request row to generate feedback data';
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Full syntax highlighting for JSON blocks
|
|
374
|
-
function highlightJson(str) {
|
|
375
|
-
return str
|
|
376
|
-
.replace(/&/g, '&').replace(/[<]/g, '<').replace(/[>]/g, '>')
|
|
377
|
-
.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\\b(true|false|null)\\b|-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?)/g, function(match) {
|
|
378
|
-
if (/^"/.test(match)) {
|
|
379
|
-
if (/:$/.test(match)) return '<span class="json-key">' + match + '</span>';
|
|
380
|
-
return '<span class="json-str">' + match + '</span>';
|
|
381
|
-
}
|
|
382
|
-
return '<span class="json-num">' + match + '</span>';
|
|
383
|
-
});
|
|
384
|
-
}
|
|
352
|
+
function highlight(str) {
|
|
353
|
+
return str
|
|
354
|
+
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
|
355
|
+
.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false)\b|\bnull\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function(m) {
|
|
356
|
+
if (/^"/.test(m)) return /:$/.test(m)
|
|
357
|
+
? '<span class="json-key">' + m + '</span>'
|
|
358
|
+
: '<span class="json-str">' + m + '</span>';
|
|
359
|
+
if (/true|false/.test(m)) return '<span class="json-bool">' + m + '</span>';
|
|
360
|
+
if (/null/.test(m)) return '<span class="json-null">' + m + '</span>';
|
|
361
|
+
return '<span class="json-num">' + m + '</span>';
|
|
362
|
+
});
|
|
363
|
+
}
|
|
385
364
|
|
|
386
|
-
function showToast(msg) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
365
|
+
function showToast(msg) {
|
|
366
|
+
const t = document.getElementById('toast');
|
|
367
|
+
t.textContent = msg;
|
|
368
|
+
t.classList.add('show');
|
|
369
|
+
setTimeout(() => t.classList.remove('show'), 2500);
|
|
370
|
+
}
|
|
392
371
|
|
|
393
|
-
buildSidebar();
|
|
372
|
+
buildSidebar();
|
|
394
373
|
</script>
|
|
395
374
|
</body>
|
|
396
|
-
</html
|
|
397
|
-
`;
|
|
375
|
+
</html>`;
|
|
398
376
|
}
|
|
377
|
+
|
|
378
|
+
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,204 @@
|
|
|
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
|
+
// handle rename (email: userEmail) and default (name = '')
|
|
45
|
+
const name = part.split(':')[0].split('=')[0].trim();
|
|
46
|
+
if (name && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) && !seen.has(name)) {
|
|
47
|
+
seen.set(name, buildField(name));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Pattern 2 — property access: req.body.email / req.body['email']
|
|
53
|
+
const accessRe = /req\.body\.([a-zA-Z_$][a-zA-Z0-9_$]*)|req\.body\[['"]([a-zA-Z_$][a-zA-Z0-9_$]*)['"]]/g;
|
|
54
|
+
while ((m = accessRe.exec(source)) !== null) {
|
|
55
|
+
const name = m[1] || m[2];
|
|
56
|
+
if (name && !seen.has(name)) seen.set(name, buildField(name));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return Array.from(seen.values());
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Path-based fallback fields ───────────────────────────────────────────────
|
|
66
|
+
function fallbackFields(path) {
|
|
67
|
+
const p = path.toLowerCase();
|
|
68
|
+
|
|
69
|
+
if (p.includes('login') || p.includes('signin') || p.includes('auth/login')) {
|
|
70
|
+
return ['email', 'password'].map(buildField);
|
|
71
|
+
}
|
|
72
|
+
if (p.includes('register') || p.includes('signup') || p.includes('auth/register')) {
|
|
73
|
+
return ['username', 'email', 'password'].map(buildField);
|
|
74
|
+
}
|
|
75
|
+
if (p.includes('user')) {
|
|
76
|
+
return ['username', 'email', 'password'].map(buildField);
|
|
77
|
+
}
|
|
78
|
+
if (p.includes('product')) {
|
|
79
|
+
return ['name', 'price', 'stock'].map(buildField);
|
|
80
|
+
}
|
|
81
|
+
if (p.includes('order')) {
|
|
82
|
+
return ['productId', 'quantity', 'address'].map(buildField);
|
|
83
|
+
}
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── Extract router prefix from Express layer regexp ─────────────────────────
|
|
88
|
+
function extractRouterPrefix(layer) {
|
|
89
|
+
if (!layer.regexp) return '';
|
|
90
|
+
const src = layer.regexp.source;
|
|
91
|
+
|
|
92
|
+
// Express serialises router paths as: ^\/<prefix>(?:\/(?=$))?(?=\/|$)
|
|
93
|
+
const patterns = [
|
|
94
|
+
/^\^\\\/([^\\?$]+)/, // common format
|
|
95
|
+
/^\^\\\/([a-zA-Z0-9_/-]+)/, // simple paths
|
|
96
|
+
];
|
|
97
|
+
for (const re of patterns) {
|
|
98
|
+
const m = re.exec(src);
|
|
99
|
+
if (m && m[1]) {
|
|
100
|
+
return '/' + m[1].replace(/\\\//g, '/').replace(/\\/g, '');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Fallback: check .regexp property stored by Express (Express 5 style)
|
|
104
|
+
if (layer.regexp && layer.regexp.fast_slash) return '';
|
|
105
|
+
return '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Walk the Express router stack recursively ────────────────────────────────
|
|
109
|
+
function parseStack(stack, detectedEndpoints, prefix = '') {
|
|
110
|
+
if (!Array.isArray(stack)) return;
|
|
111
|
+
|
|
112
|
+
for (const layer of stack) {
|
|
113
|
+
// ── Named route (app.get / app.post …) ──────────────────────────────────
|
|
114
|
+
if (layer.route) {
|
|
115
|
+
const rawPath = typeof layer.route.path === 'string'
|
|
116
|
+
? layer.route.path
|
|
117
|
+
: (layer.route.path ? String(layer.route.path) : '');
|
|
118
|
+
|
|
119
|
+
const fullPath = (prefix + rawPath).replace(/\/+/g, '/') || '/';
|
|
120
|
+
|
|
121
|
+
// skip the tester's own route
|
|
122
|
+
if (fullPath.startsWith('/api/tester')) continue;
|
|
123
|
+
|
|
124
|
+
const methods = Object.keys(layer.route.methods || {});
|
|
125
|
+
|
|
126
|
+
for (const method of methods) {
|
|
127
|
+
const httpMethod = method.toUpperCase();
|
|
128
|
+
const key = `${httpMethod}::${fullPath}`;
|
|
129
|
+
|
|
130
|
+
// ── Path params (:id, :slug …) ─────────────────────────────────────
|
|
131
|
+
const pathParams = [];
|
|
132
|
+
const paramRe = /:([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
|
|
133
|
+
let pm;
|
|
134
|
+
while ((pm = paramRe.exec(fullPath)) !== null) {
|
|
135
|
+
pathParams.push({
|
|
136
|
+
name: pm[1],
|
|
137
|
+
label: pm[1].charAt(0).toUpperCase() + pm[1].slice(1),
|
|
138
|
+
placeholder: 'value'
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Body fields ────────────────────────────────────────────────────
|
|
143
|
+
let bodyFields = [];
|
|
144
|
+
if (['POST', 'PUT', 'PATCH'].includes(httpMethod)) {
|
|
145
|
+
const handlers = (layer.route.stack || []).map(sl => sl.handle).filter(Boolean);
|
|
146
|
+
for (const handler of handlers) {
|
|
147
|
+
bodyFields.push(...extractBodyFields(handler));
|
|
148
|
+
}
|
|
149
|
+
// deduplicate by name
|
|
150
|
+
const seen = new Map();
|
|
151
|
+
bodyFields = bodyFields.filter(f => {
|
|
152
|
+
if (seen.has(f.name)) return false;
|
|
153
|
+
seen.set(f.name, true);
|
|
154
|
+
return true;
|
|
155
|
+
});
|
|
156
|
+
// fallback if extraction produced nothing
|
|
157
|
+
if (bodyFields.length === 0) {
|
|
158
|
+
bodyFields = fallbackFields(fullPath);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
detectedEndpoints[key] = {
|
|
163
|
+
method: httpMethod,
|
|
164
|
+
path: fullPath,
|
|
165
|
+
title: `${httpMethod} ${fullPath}`,
|
|
166
|
+
desc: `Auto-discovered endpoint — ${fullPath}`,
|
|
167
|
+
params: pathParams,
|
|
168
|
+
fields: bodyFields,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Nested router (app.use('/prefix', router)) ───────────────────────────
|
|
174
|
+
else if (layer.name === 'router' && layer.handle && layer.handle.stack) {
|
|
175
|
+
const routerPrefix = extractRouterPrefix(layer);
|
|
176
|
+
parseStack(layer.handle.stack, detectedEndpoints, prefix + routerPrefix);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ─── Middleware ───────────────────────────────────────────────────────────────
|
|
182
|
+
function endtesterExpress() {
|
|
183
|
+
return function monkeyTesterMiddleware(req, res, next) {
|
|
184
|
+
if (req.path !== '/api/tester' && req.path !== '/api/tester/') {
|
|
185
|
+
return next();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const app = req.app;
|
|
189
|
+
const detectedEndpoints = {};
|
|
190
|
+
|
|
191
|
+
const rootStack =
|
|
192
|
+
(app._router && app._router.stack) || // Express 4
|
|
193
|
+
(app.router && app.router.stack) || // Express 5 preview
|
|
194
|
+
[];
|
|
195
|
+
|
|
196
|
+
parseStack(rootStack, detectedEndpoints);
|
|
197
|
+
|
|
198
|
+
const html = getHtmlTemplate(detectedEndpoints);
|
|
199
|
+
res.setHeader('Content-Type', 'text/html');
|
|
200
|
+
return res.send(html);
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = { endtesterExpress };
|