@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.
Files changed (4) hide show
  1. package/htmlTemplate.js +243 -263
  2. package/index.js +3 -244
  3. package/monkey.js +204 -0
  4. package/package.json +1 -1
package/htmlTemplate.js CHANGED
@@ -1,13 +1,14 @@
1
- export function getHtmlTemplate(endpoints) {
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>Endtester — API Environment</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; border-radius: var(--radius); width: 220px; outline: none;
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 { background: #1a3a22; color: #6ba05a; }
84
- .POST { background: #1a2e3a; color: #5a86c0; }
85
- .PUT, .PATCH { background: #3a2e10; color: #e8a838; }
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 { font-family: 'DM Mono', monospace; font-size: 13px; color: var(--text-dim); margin-bottom: 24px; display: flex; align-items: center; gap: 8px; }
98
- .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; }
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], input[type=tel], input[type=url], select {
108
- background: var(--surface2); border: 1px solid var(--border); color: var(--text); font-family: 'DM Sans', sans-serif; font-size: 13px; padding: 8px 12px; border-radius: var(--radius); width: 100%; outline: none; transition: border-color 0.2s;
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 { background: #1a3a22; color: #6ba05a; }
129
- .status-err { background: #3a1a14; color: #d45c3c; }
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
- flex: 1;
134
- overflow-y: auto;
135
- overflow-x: hidden;
136
- padding: 0;
137
- background: #0d0b08;
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="endtester-data-vault" data-payload="${safeJsonString}" style="display: none;"></div>
153
+ <div id="__monkey_data__" data-payload="${safeJsonString}" style="display:none;"></div>
173
154
 
174
155
  <header>
175
- <div class="logo">Endtester <span>Application Runtime Sandbox</span></div>
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 AUTH</label>
183
- <input id="jwt-input" type="text" placeholder="Token value...">
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 Output</span>
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 row to generate feedback data</div>
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 rawPayload = document.getElementById('endtester-data-vault').getAttribute('data-payload');
206
- const ENDPOINTS = JSON.parse(atob(rawPayload));
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
- let currentEp = '';
191
+ // ── Sidebar ────────────────────────────────────────────────────────────────
192
+ function buildSidebar() {
193
+ const sidebar = document.getElementById('sidebar-nav');
194
+ const keys = Object.keys(ENDPOINTS);
209
195
 
210
- document.getElementById('base-url').value = window.location.origin;
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
- function buildSidebar() {
213
- const sidebar = document.getElementById('sidebar-nav');
214
- const keys = Object.keys(ENDPOINTS);
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
- if (keys.length === 0) {
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
- keys.forEach((key, index) => {
222
+ // ── Panel ──────────────────────────────────────────────────────────────────
223
+ function renderPanel(key) {
224
+ currentKey = key;
222
225
  const ep = ENDPOINTS[key];
223
- const div = document.createElement('div');
224
- div.className = index === 0 ? 'nav-item active' : 'nav-item';
225
- div.setAttribute('data-ep', key);
226
- div.innerHTML = '<span class="method-badge ' + ep.method + '">' + ep.method + '</span><span class="nav-label">' + ep.path + '</span>';
227
- div.addEventListener('click', () => {
228
- document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
229
- div.classList.add('active');
230
- clearResponse();
231
- renderPanel(key);
232
- });
233
- sidebar.appendChild(div);
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
- if (keys.length > 0) renderPanel(keys[0]);
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
- function renderPanel(epKey) {
240
- currentEp = epKey;
241
- const ep = ENDPOINTS[epKey];
242
- const main = document.getElementById('main-panel');
243
- if (!ep) return;
244
-
245
- // Escaping using triple backslashes ensures client variables evaluate properly at runtime
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()">&#9658; Send</button>' +
264
+ '<button class="btn btn-secondary" onclick="clearResponse()">Clear</button>' +
265
+ '</div>';
266
+
267
+ main.innerHTML = html;
267
268
  }
268
269
 
269
- if (ep.fields && ep.fields.length) {
270
- html += \`<div class="form-section"><div class="form-section-title">HTTP JSON Request Payload Parameters</div>\`;
271
- ep.fields.forEach(function(f) {
272
- const type = f.type || 'text';
273
- const ph = f.placeholder ? ' placeholder="' + f.placeholder + '"' : '';
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
- html += \`
285
- <div class="btn-row">
286
- <button class="btn" onclick="sendRequest()">Execute Route</button>
287
- <button class="btn btn-secondary" onclick="clearResponse()">Clear Context</button>
288
- </div>
289
- \`;
277
+ // ── Request ────────────────────────────────────────────────────────────────
278
+ async function sendRequest() {
279
+ const ep = ENDPOINTS[currentKey];
280
+ let path = ep.path;
290
281
 
291
- main.innerHTML = html;
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
- async function sendRequest() {
295
- const ep = ENDPOINTS[currentEp];
296
- let path = ep.path;
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
- if (ep.params) {
299
- for (const p of ep.params) {
300
- const val = document.getElementById('param-' + p.name)?.value.trim();
301
- if (!val) { showToast('Warning: Parameter ' + p.label + ' is required'); return; }
302
- path = path.replace(':' + p.name, encodeURIComponent(val));
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
- const baseUrl = document.getElementById('base-url').value.replace(/[/]+$/, '');
307
- const url = baseUrl + path;
308
- const headers = { 'Content-Type': 'application/json' };
309
-
310
- const jwt = document.getElementById('jwt-input').value.trim();
311
- if (jwt) headers['Authorization'] = 'Bearer ' + jwt;
312
-
313
- let body = undefined;
314
- if (ep.fields && ep.fields.length && ['POST', 'PUT', 'PATCH'].includes(ep.method)) {
315
- const jsonPayload = {};
316
- for (const f of ep.fields) {
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
- setResponse(null, 'loading');
330
- const start = Date.now();
331
-
332
- try {
333
- const res = await fetch(url, { method: ep.method, headers, body });
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
- if (state === 'loading') {
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.innerHTML = 'Executing transmission...';
353
- return;
349
+ body.textContent = 'Execute a request to see the response';
354
350
  }
355
351
 
356
- badge.className = 'status-badge ' + (state === 'ok' ? 'status-ok' : 'status-err');
357
- badge.textContent = status + ' · ' + ms + 'ms';
358
- body.className = 'response-body';
359
-
360
- const outputString = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
361
- body.innerHTML = '<pre class="json-render-block">' + highlightJson(outputString) + '</pre>';
362
- }
363
-
364
- function clearResponse() {
365
- const badge = document.getElementById('status-badge');
366
- const body = document.getElementById('response-body');
367
- badge.className = 'status-badge status-idle';
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, '&amp;').replace(/[<]/g, '&lt;').replace(/[>]/g, '&gt;')
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
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
- const t = document.getElementById('toast');
388
- t.textContent = msg;
389
- t.classList.add('show');
390
- setTimeout(() => t.classList.remove('show'), 2500);
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
- import { getHtmlTemplate } from './htmlTemplate.js';
1
+ 'use strict';
2
2
 
3
- export function endtesterExpress() {
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
- const expressApp = req.app;
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aimeloic/monkey-tester",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "Auto route scanning visual runner dashboard.",
5
5
  "main": "index.js",
6
6
  "type": "module",