kempo-server 1.7.6 → 1.7.8
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/package.json +1 -1
- package/src/router.js +48 -39
- package/tests/router-wildcard-double-asterisk.node-test.js +19 -20
- package/tests/router-wildcard.node-test.js +13 -13
- package/tests/router.node-test.js +46 -0
- package/tests/test-server-root/docs/.config.json +2 -3
- package/tests/test-server-root/docs/src/components/Button.js +1 -1
- package/tests/test-server-root/docs/src/nested/file.js +1 -1
- package/tests/test-server-root/docs/src/utils/helpers/format.js +1 -1
- package/tests/test-server-root/src/components/Import.js +1 -0
- package/tests/test-server-root/src/deep/nested/file.js +1 -0
- package/tests/test-server-root/src/file.js +1 -1
- package/tests/test-server-root/src/nested/file.js +1 -1
- package/tests/test-server-root/src/utils/helpers.js +1 -0
- package/tests/test-server-root/README.md +0 -118
package/package.json
CHANGED
package/src/router.js
CHANGED
|
@@ -16,9 +16,9 @@ import {
|
|
|
16
16
|
} from './builtinMiddleware.js';
|
|
17
17
|
|
|
18
18
|
export default async (flags, log) => {
|
|
19
|
-
log('Initializing router',
|
|
19
|
+
log('Initializing router', 3);
|
|
20
20
|
const rootPath = path.isAbsolute(flags.root) ? flags.root : path.join(process.cwd(), flags.root);
|
|
21
|
-
log(`Root path: ${rootPath}`,
|
|
21
|
+
log(`Root path: ${rootPath}`, 3);
|
|
22
22
|
|
|
23
23
|
let config = defaultConfig;
|
|
24
24
|
try {
|
|
@@ -28,25 +28,25 @@ export default async (flags, log) => {
|
|
|
28
28
|
? configFileName
|
|
29
29
|
: path.join(rootPath, configFileName);
|
|
30
30
|
|
|
31
|
-
log(`Config file name: ${configFileName}`,
|
|
32
|
-
log(`Config path: ${configPath}`,
|
|
31
|
+
log(`Config file name: ${configFileName}`, 3);
|
|
32
|
+
log(`Config path: ${configPath}`, 3);
|
|
33
33
|
|
|
34
34
|
// Validate that config file is within the server root directory
|
|
35
35
|
// Allow absolute paths (user explicitly specified location)
|
|
36
36
|
if (!path.isAbsolute(configFileName)) {
|
|
37
37
|
const relativeConfigPath = path.relative(rootPath, configPath);
|
|
38
|
-
log(`Relative config path: ${relativeConfigPath}`,
|
|
39
|
-
log(`Starts with '..': ${relativeConfigPath.startsWith('..')}`,
|
|
38
|
+
log(`Relative config path: ${relativeConfigPath}`, 4);
|
|
39
|
+
log(`Starts with '..': ${relativeConfigPath.startsWith('..')}`, 4);
|
|
40
40
|
if (relativeConfigPath.startsWith('..') || path.isAbsolute(relativeConfigPath)) {
|
|
41
|
-
log(`Validation failed - throwing error`,
|
|
41
|
+
log(`Validation failed - throwing error`, 4);
|
|
42
42
|
throw new Error(`Config file must be within the server root directory. Config path: ${configPath}, Root path: ${rootPath}`);
|
|
43
43
|
}
|
|
44
|
-
log(`Validation passed`,
|
|
44
|
+
log(`Validation passed`, 4);
|
|
45
45
|
} else {
|
|
46
|
-
log(`Config file name is absolute, skipping validation`,
|
|
46
|
+
log(`Config file name is absolute, skipping validation`, 4);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
log(`Loading config from: ${configPath}`,
|
|
49
|
+
log(`Loading config from: ${configPath}`, 3);
|
|
50
50
|
const configContent = await readFile(configPath, 'utf8');
|
|
51
51
|
const userConfig = JSON.parse(configContent);
|
|
52
52
|
config = {
|
|
@@ -70,14 +70,14 @@ export default async (flags, log) => {
|
|
|
70
70
|
...userConfig.cache
|
|
71
71
|
}
|
|
72
72
|
};
|
|
73
|
-
log('User config loaded and merged with defaults',
|
|
73
|
+
log('User config loaded and merged with defaults', 3);
|
|
74
74
|
} catch (e){
|
|
75
75
|
// Only fall back to default config for file reading/parsing errors
|
|
76
76
|
// Let validation errors propagate up
|
|
77
77
|
if (e.message.includes('Config file must be within the server root directory')) {
|
|
78
78
|
throw e;
|
|
79
79
|
}
|
|
80
|
-
log('Using default config (no config file found)',
|
|
80
|
+
log('Using default config (no config file found)', 3);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/*
|
|
@@ -88,10 +88,10 @@ export default async (flags, log) => {
|
|
|
88
88
|
dis.add("\\.config$");
|
|
89
89
|
dis.add("\\.git/");
|
|
90
90
|
config.disallowedRegex = [...dis];
|
|
91
|
-
log(`Config loaded with ${config.disallowedRegex.length} disallowed patterns`,
|
|
91
|
+
log(`Config loaded with ${config.disallowedRegex.length} disallowed patterns`, 3);
|
|
92
92
|
|
|
93
93
|
let files = await getFiles(rootPath, config, log);
|
|
94
|
-
log(`Initial scan found ${files.length} files`,
|
|
94
|
+
log(`Initial scan found ${files.length} files`, 2);
|
|
95
95
|
|
|
96
96
|
// Initialize middleware runner
|
|
97
97
|
const middlewareRunner = new MiddlewareRunner();
|
|
@@ -99,32 +99,32 @@ export default async (flags, log) => {
|
|
|
99
99
|
// Load built-in middleware based on config
|
|
100
100
|
if (config.middleware?.cors?.enabled) {
|
|
101
101
|
middlewareRunner.use(corsMiddleware(config.middleware.cors));
|
|
102
|
-
log('CORS middleware enabled',
|
|
102
|
+
log('CORS middleware enabled', 3);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
if (config.middleware?.compression?.enabled) {
|
|
106
106
|
middlewareRunner.use(compressionMiddleware(config.middleware.compression));
|
|
107
|
-
log('Compression middleware enabled',
|
|
107
|
+
log('Compression middleware enabled', 3);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
if (config.middleware?.rateLimit?.enabled) {
|
|
111
111
|
middlewareRunner.use(rateLimitMiddleware(config.middleware.rateLimit));
|
|
112
|
-
log('Rate limit middleware enabled',
|
|
112
|
+
log('Rate limit middleware enabled', 3);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
if (config.middleware?.security?.enabled) {
|
|
116
116
|
middlewareRunner.use(securityMiddleware(config.middleware.security));
|
|
117
|
-
log('Security middleware enabled',
|
|
117
|
+
log('Security middleware enabled', 3);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
if (config.middleware?.logging?.enabled) {
|
|
121
121
|
middlewareRunner.use(loggingMiddleware(config.middleware.logging, log));
|
|
122
|
-
log('Logging middleware enabled',
|
|
122
|
+
log('Logging middleware enabled', 3);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
// Load custom middleware files
|
|
126
126
|
if (config.middleware?.custom && config.middleware.custom.length > 0) {
|
|
127
|
-
log(`Loading ${config.middleware.custom.length} custom middleware files`,
|
|
127
|
+
log(`Loading ${config.middleware.custom.length} custom middleware files`, 3);
|
|
128
128
|
|
|
129
129
|
for (const middlewarePath of config.middleware.custom) {
|
|
130
130
|
try {
|
|
@@ -134,7 +134,7 @@ export default async (flags, log) => {
|
|
|
134
134
|
|
|
135
135
|
if (typeof customMiddleware === 'function') {
|
|
136
136
|
middlewareRunner.use(customMiddleware(config.middleware));
|
|
137
|
-
log(`Custom middleware loaded: ${middlewarePath}`,
|
|
137
|
+
log(`Custom middleware loaded: ${middlewarePath}`, 3);
|
|
138
138
|
} else {
|
|
139
139
|
log(`Custom middleware error: ${middlewarePath} does not export a default function`, 1);
|
|
140
140
|
}
|
|
@@ -159,7 +159,7 @@ export default async (flags, log) => {
|
|
|
159
159
|
const wildcardRoutes = new Map();
|
|
160
160
|
|
|
161
161
|
if (config.customRoutes && Object.keys(config.customRoutes).length > 0) {
|
|
162
|
-
log(`Processing ${Object.keys(config.customRoutes).length} custom routes`,
|
|
162
|
+
log(`Processing ${Object.keys(config.customRoutes).length} custom routes`, 3);
|
|
163
163
|
for (const [urlPath, filePath] of Object.entries(config.customRoutes)) {
|
|
164
164
|
// Check if this is a wildcard route
|
|
165
165
|
if (urlPath.includes('*')) {
|
|
@@ -167,12 +167,12 @@ export default async (flags, log) => {
|
|
|
167
167
|
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(rootPath, filePath);
|
|
168
168
|
// Store wildcard routes separately for pattern matching
|
|
169
169
|
wildcardRoutes.set(urlPath, resolvedPath);
|
|
170
|
-
log(`Wildcard route mapped: ${urlPath} -> ${resolvedPath}`,
|
|
170
|
+
log(`Wildcard route mapped: ${urlPath} -> ${resolvedPath}`, 3);
|
|
171
171
|
} else {
|
|
172
172
|
// Resolve the file path relative to rootPath if relative, otherwise use absolute path
|
|
173
173
|
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(rootPath, filePath);
|
|
174
174
|
customRoutes.set(urlPath, resolvedPath);
|
|
175
|
-
log(`Custom route mapped: ${urlPath} -> ${resolvedPath}`,
|
|
175
|
+
log(`Custom route mapped: ${urlPath} -> ${resolvedPath}`, 3);
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
}
|
|
@@ -259,27 +259,36 @@ export default async (flags, log) => {
|
|
|
259
259
|
log(`Path ${requestPath} added to dynamic blacklist after ${newAttempts} failed attempts`, 1);
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
log(`Rescan attempt ${newAttempts}/${config.maxRescanAttempts} for: ${requestPath}`,
|
|
262
|
+
log(`Rescan attempt ${newAttempts}/${config.maxRescanAttempts} for: ${requestPath}`, 3);
|
|
263
263
|
return newAttempts;
|
|
264
264
|
};
|
|
265
265
|
|
|
266
266
|
const requestHandler = async (req, res) => {
|
|
267
267
|
await middlewareRunner.run(req, res, async () => {
|
|
268
268
|
const requestPath = req.url.split('?')[0];
|
|
269
|
-
log(`${req.method} ${requestPath}`,
|
|
269
|
+
log(`${req.method} ${requestPath}`, 4);
|
|
270
270
|
|
|
271
271
|
|
|
272
272
|
// Check custom routes first (allow outside rootPath)
|
|
273
|
-
log(`customRoutes keys: ${Array.from(customRoutes.keys()).join(', ')}`,
|
|
273
|
+
log(`customRoutes keys: ${Array.from(customRoutes.keys()).join(', ')}`, 4);
|
|
274
274
|
// Normalize requestPath and keys for matching
|
|
275
275
|
const normalizePath = p => {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
276
|
+
try {
|
|
277
|
+
let np = decodeURIComponent(p);
|
|
278
|
+
if (!np.startsWith('/')) np = '/' + np;
|
|
279
|
+
if (np.length > 1 && np.endsWith('/')) np = np.slice(0, -1);
|
|
280
|
+
return np;
|
|
281
|
+
} catch (e) {
|
|
282
|
+
log(`Warning: Failed to decode URI component "${p}": ${e.message}`, 1);
|
|
283
|
+
// Return the original path if decoding fails
|
|
284
|
+
let np = p;
|
|
285
|
+
if (!np.startsWith('/')) np = '/' + np;
|
|
286
|
+
if (np.length > 1 && np.endsWith('/')) np = np.slice(0, -1);
|
|
287
|
+
return np;
|
|
288
|
+
}
|
|
280
289
|
};
|
|
281
290
|
const normalizedRequestPath = normalizePath(requestPath);
|
|
282
|
-
log(`Normalized requestPath: ${normalizedRequestPath}`,
|
|
291
|
+
log(`Normalized requestPath: ${normalizedRequestPath}`, 4);
|
|
283
292
|
let matchedKey = null;
|
|
284
293
|
for (const key of customRoutes.keys()) {
|
|
285
294
|
if (normalizePath(key) === normalizedRequestPath) {
|
|
@@ -289,14 +298,14 @@ export default async (flags, log) => {
|
|
|
289
298
|
}
|
|
290
299
|
if (matchedKey) {
|
|
291
300
|
const customFilePath = customRoutes.get(matchedKey);
|
|
292
|
-
log(`Serving custom route: ${normalizedRequestPath} -> ${customFilePath}`,
|
|
301
|
+
log(`Serving custom route: ${normalizedRequestPath} -> ${customFilePath}`, 3);
|
|
293
302
|
try {
|
|
294
303
|
const { stat } = await import('fs/promises');
|
|
295
304
|
try {
|
|
296
305
|
await stat(customFilePath);
|
|
297
|
-
log(`Custom route file exists: ${customFilePath}`,
|
|
306
|
+
log(`Custom route file exists: ${customFilePath}`, 4);
|
|
298
307
|
} catch (e) {
|
|
299
|
-
log(`Custom route file does NOT exist: ${customFilePath}`,
|
|
308
|
+
log(`Custom route file does NOT exist: ${customFilePath}`, 1);
|
|
300
309
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
301
310
|
res.end('Custom route file not found');
|
|
302
311
|
return;
|
|
@@ -309,7 +318,7 @@ export default async (flags, log) => {
|
|
|
309
318
|
res.end(fileContent);
|
|
310
319
|
return; // Successfully served custom route
|
|
311
320
|
} catch (error) {
|
|
312
|
-
log(`Error serving custom route ${normalizedRequestPath}: ${error.message}`,
|
|
321
|
+
log(`Error serving custom route ${normalizedRequestPath}: ${error.message}`, 1);
|
|
313
322
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
314
323
|
res.end('Internal Server Error');
|
|
315
324
|
return;
|
|
@@ -320,17 +329,17 @@ export default async (flags, log) => {
|
|
|
320
329
|
const wildcardMatch = findWildcardRoute(requestPath);
|
|
321
330
|
if (wildcardMatch) {
|
|
322
331
|
const resolvedFilePath = resolveWildcardPath(wildcardMatch.filePath, wildcardMatch.matches);
|
|
323
|
-
log(`Serving wildcard route: ${requestPath} -> ${resolvedFilePath}`,
|
|
332
|
+
log(`Serving wildcard route: ${requestPath} -> ${resolvedFilePath}`, 3);
|
|
324
333
|
try {
|
|
325
334
|
const fileContent = await readFile(resolvedFilePath);
|
|
326
335
|
const fileExtension = path.extname(resolvedFilePath).toLowerCase().slice(1);
|
|
327
336
|
const mimeType = config.allowedMimes[fileExtension] || 'application/octet-stream';
|
|
328
|
-
log(`Serving wildcard file as ${mimeType} (${fileContent.length} bytes)`,
|
|
337
|
+
log(`Serving wildcard file as ${mimeType} (${fileContent.length} bytes)`, 4);
|
|
329
338
|
res.writeHead(200, { 'Content-Type': mimeType });
|
|
330
339
|
res.end(fileContent);
|
|
331
340
|
return; // Successfully served wildcard route
|
|
332
341
|
} catch (error) {
|
|
333
|
-
log(`Error serving wildcard route ${requestPath}: ${error.message}`,
|
|
342
|
+
log(`Error serving wildcard route ${requestPath}: ${error.message}`, 1);
|
|
334
343
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
335
344
|
res.end('Internal Server Error');
|
|
336
345
|
return;
|
|
@@ -13,8 +13,8 @@ export default {
|
|
|
13
13
|
const fileC = await write(dir, 'src/deep/nested/folder/file.js', 'export const deep = true');
|
|
14
14
|
|
|
15
15
|
// Create files in docs directory that should be ignored when custom routes match
|
|
16
|
-
await write(dir, 'docs/src/components/Button.js', 'WRONG FILE');
|
|
17
|
-
await write(dir, 'docs/src/utils/helpers/format.js', 'WRONG FILE');
|
|
16
|
+
await write(dir, 'docs/src/components/Button.js', '// WRONG FILE');
|
|
17
|
+
await write(dir, 'docs/src/utils/helpers/format.js', '// WRONG FILE');
|
|
18
18
|
|
|
19
19
|
const prev = process.cwd();
|
|
20
20
|
process.chdir(dir);
|
|
@@ -70,12 +70,12 @@ export default {
|
|
|
70
70
|
'single asterisk only matches single path segments': async ({pass, fail, log}) => {
|
|
71
71
|
try {
|
|
72
72
|
await withTempDir(async (dir) => {
|
|
73
|
-
// Create
|
|
74
|
-
await write(dir, 'src/file.js', 'single level');
|
|
75
|
-
await write(dir, 'src/nested/file.js', 'nested level');
|
|
73
|
+
// Create files at different nesting levels
|
|
74
|
+
await write(dir, 'src/file.js', '// single level');
|
|
75
|
+
await write(dir, 'src/nested/file.js', '// nested level');
|
|
76
76
|
|
|
77
|
-
// Create fallback file
|
|
78
|
-
await write(dir, 'docs/src/nested/file.js', 'fallback file');
|
|
77
|
+
// Create docs directory with fallback file
|
|
78
|
+
await write(dir, 'docs/src/nested/file.js', '// fallback file');
|
|
79
79
|
|
|
80
80
|
const prev = process.cwd();
|
|
81
81
|
process.chdir(dir);
|
|
@@ -103,13 +103,13 @@ export default {
|
|
|
103
103
|
const r1 = await httpGet(`http://localhost:${port}/src/file.js`);
|
|
104
104
|
log('single level status: ' + r1.res.statusCode);
|
|
105
105
|
if(r1.res.statusCode !== 200) throw new Error('single level 200');
|
|
106
|
-
if(r1.body.toString() !== 'single level') throw new Error('single level content');
|
|
106
|
+
if(r1.body.toString() !== '// single level') throw new Error('single level content');
|
|
107
107
|
|
|
108
108
|
// Nested level should NOT work with single asterisk, should fall back to docs
|
|
109
109
|
const r2 = await httpGet(`http://localhost:${port}/src/nested/file.js`);
|
|
110
110
|
log('nested level status: ' + r2.res.statusCode);
|
|
111
111
|
if(r2.res.statusCode !== 200) throw new Error('nested level 200');
|
|
112
|
-
if(r2.body.toString() !== 'fallback file') throw new Error('nested level should use fallback');
|
|
112
|
+
if(r2.body.toString() !== '// fallback file') throw new Error('nested level should use fallback');
|
|
113
113
|
|
|
114
114
|
} finally {
|
|
115
115
|
server.close();
|
|
@@ -125,12 +125,12 @@ export default {
|
|
|
125
125
|
'wildcard routes take precedence over static files': async ({pass, fail, log}) => {
|
|
126
126
|
try {
|
|
127
127
|
await withTempDir(async (dir) => {
|
|
128
|
-
// Create
|
|
129
|
-
await write(dir, 'custom/data.json', '{"source": "custom"}');
|
|
130
|
-
|
|
131
|
-
// Create static files in docs that should be overridden
|
|
128
|
+
// Create static file in docs
|
|
132
129
|
await write(dir, 'docs/api/data.json', '{"source": "static"}');
|
|
133
130
|
|
|
131
|
+
// Create custom file outside docs
|
|
132
|
+
await write(dir, 'custom/data.json', '{"source": "custom"}');
|
|
133
|
+
|
|
134
134
|
const prev = process.cwd();
|
|
135
135
|
process.chdir(dir);
|
|
136
136
|
|
|
@@ -174,13 +174,11 @@ export default {
|
|
|
174
174
|
'wildcard routes serve nested files from server root': async ({pass, fail, log}) => {
|
|
175
175
|
try {
|
|
176
176
|
await withTempDir(async (dir) => {
|
|
177
|
-
// Create
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
// Create index.html in server root
|
|
183
|
-
await write(dir, 'index.html', '<h1>Home</h1>');
|
|
177
|
+
// Create additional test files needed for this specific test
|
|
178
|
+
await write(dir, 'src/components/Import.js', 'export default ImportComponent');
|
|
179
|
+
await write(dir, 'src/utils/helpers.js', 'export const helpers = {}');
|
|
180
|
+
await write(dir, 'src/deep/nested/file.js', 'export const deep = true');
|
|
181
|
+
await write(dir, 'index.html', '<html><body>Test Index</body></html>');
|
|
184
182
|
|
|
185
183
|
const prev = process.cwd();
|
|
186
184
|
process.chdir(dir);
|
|
@@ -220,6 +218,7 @@ export default {
|
|
|
220
218
|
const r3 = await httpGet(`http://localhost:${port}/src/deep/nested/file.js`);
|
|
221
219
|
log('deep nested file status: ' + r3.res.statusCode);
|
|
222
220
|
if(r3.res.statusCode !== 200) throw new Error(`Expected 200 for deep nested file, got ${r3.res.statusCode}`);
|
|
221
|
+
if(r3.body.toString() !== 'export const deep = true') throw new Error('Deep nested file content mismatch');
|
|
223
222
|
|
|
224
223
|
// Test that index.html still works (non-wildcard route)
|
|
225
224
|
const r4 = await httpGet(`http://localhost:${port}/index.html`);
|
|
@@ -13,8 +13,8 @@ export default {
|
|
|
13
13
|
const fileC = await write(dir, 'src/deep/nested/folder/file.js', 'export const deep = true');
|
|
14
14
|
|
|
15
15
|
// Create files in docs directory that should be ignored when custom routes match
|
|
16
|
-
await write(dir, 'docs/src/components/Button.js', 'WRONG FILE');
|
|
17
|
-
await write(dir, 'docs/src/utils/helpers/format.js', 'WRONG FILE');
|
|
16
|
+
await write(dir, 'docs/src/components/Button.js', '// WRONG FILE');
|
|
17
|
+
await write(dir, 'docs/src/utils/helpers/format.js', '// WRONG FILE');
|
|
18
18
|
|
|
19
19
|
const prev = process.cwd();
|
|
20
20
|
process.chdir(dir);
|
|
@@ -70,12 +70,12 @@ export default {
|
|
|
70
70
|
'single asterisk only matches single path segments': async ({pass, fail, log}) => {
|
|
71
71
|
try {
|
|
72
72
|
await withTempDir(async (dir) => {
|
|
73
|
-
// Create
|
|
74
|
-
await write(dir, 'src/file.js', 'single level');
|
|
75
|
-
await write(dir, 'src/nested/file.js', 'nested level');
|
|
73
|
+
// Create files at different nesting levels
|
|
74
|
+
await write(dir, 'src/file.js', '// single level');
|
|
75
|
+
await write(dir, 'src/nested/file.js', '// nested level');
|
|
76
76
|
|
|
77
|
-
// Create fallback file
|
|
78
|
-
await write(dir, 'docs/src/nested/file.js', 'fallback file');
|
|
77
|
+
// Create docs directory with fallback file
|
|
78
|
+
await write(dir, 'docs/src/nested/file.js', '// fallback file');
|
|
79
79
|
|
|
80
80
|
const prev = process.cwd();
|
|
81
81
|
process.chdir(dir);
|
|
@@ -103,13 +103,13 @@ export default {
|
|
|
103
103
|
const r1 = await httpGet(`http://localhost:${port}/src/file.js`);
|
|
104
104
|
log('single level status: ' + r1.res.statusCode);
|
|
105
105
|
if(r1.res.statusCode !== 200) throw new Error('single level 200');
|
|
106
|
-
if(r1.body.toString() !== 'single level') throw new Error('single level content');
|
|
106
|
+
if(r1.body.toString() !== '// single level') throw new Error('single level content');
|
|
107
107
|
|
|
108
108
|
// Nested level should NOT work with single asterisk, should fall back to docs
|
|
109
109
|
const r2 = await httpGet(`http://localhost:${port}/src/nested/file.js`);
|
|
110
110
|
log('nested level status: ' + r2.res.statusCode);
|
|
111
111
|
if(r2.res.statusCode !== 200) throw new Error('nested level 200');
|
|
112
|
-
if(r2.body.toString() !== 'fallback file') throw new Error('nested level should use fallback');
|
|
112
|
+
if(r2.body.toString() !== '// fallback file') throw new Error('nested level should use fallback');
|
|
113
113
|
|
|
114
114
|
} finally {
|
|
115
115
|
server.close();
|
|
@@ -125,12 +125,12 @@ export default {
|
|
|
125
125
|
'wildcard routes take precedence over static files': async ({pass, fail, log}) => {
|
|
126
126
|
try {
|
|
127
127
|
await withTempDir(async (dir) => {
|
|
128
|
-
// Create
|
|
129
|
-
await write(dir, 'custom/data.json', '{"source": "custom"}');
|
|
130
|
-
|
|
131
|
-
// Create static files in docs that should be overridden
|
|
128
|
+
// Create static file in docs
|
|
132
129
|
await write(dir, 'docs/api/data.json', '{"source": "static"}');
|
|
133
130
|
|
|
131
|
+
// Create custom file outside docs
|
|
132
|
+
await write(dir, 'custom/data.json', '{"source": "custom"}');
|
|
133
|
+
|
|
134
134
|
const prev = process.cwd();
|
|
135
135
|
process.chdir(dir);
|
|
136
136
|
|
|
@@ -103,5 +103,51 @@ export default {
|
|
|
103
103
|
process.chdir(prev);
|
|
104
104
|
});
|
|
105
105
|
pass('custom+wildcard');
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
'handles malformed URLs gracefully': async ({pass, fail, log}) => {
|
|
109
|
+
await withTestDir(async (dir) => {
|
|
110
|
+
const prev = process.cwd();
|
|
111
|
+
process.chdir(dir);
|
|
112
|
+
const flags = {root: '.', logging: 0, scan: false};
|
|
113
|
+
const logFn = () => {};
|
|
114
|
+
const handler = await router(flags, logFn);
|
|
115
|
+
const server = http.createServer(handler);
|
|
116
|
+
const port = randomPort();
|
|
117
|
+
await new Promise(r => server.listen(port, r));
|
|
118
|
+
await new Promise(r => setTimeout(r, 50));
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
// Test various malformed URLs that could cause decodeURIComponent to throw
|
|
122
|
+
const malformedUrls = [
|
|
123
|
+
'/test%', // Incomplete percent encoding
|
|
124
|
+
'/test%2', // Incomplete percent encoding
|
|
125
|
+
'/test%G1', // Invalid hex characters
|
|
126
|
+
'/test%ZZ', // Invalid hex characters
|
|
127
|
+
'/test%1G', // Invalid hex characters
|
|
128
|
+
'/%E0%A4%A', // Incomplete UTF-8 sequence
|
|
129
|
+
'/%C0%80' // Overlong UTF-8 encoding
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
for (const url of malformedUrls) {
|
|
133
|
+
log(`Testing malformed URL: ${url}`);
|
|
134
|
+
const response = await httpGet(`http://localhost:${port}${url}`);
|
|
135
|
+
// Server should handle the request gracefully (return 404 or serve content)
|
|
136
|
+
// and not crash with URIError
|
|
137
|
+
if (response.res.statusCode !== 404 && response.res.statusCode !== 200) {
|
|
138
|
+
throw new Error(`Unexpected status code ${response.res.statusCode} for URL ${url}`);
|
|
139
|
+
}
|
|
140
|
+
log(`URL ${url} handled gracefully with status ${response.res.statusCode}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
server.close();
|
|
144
|
+
process.chdir(prev);
|
|
145
|
+
pass('malformed URLs handled gracefully');
|
|
146
|
+
} catch (e) {
|
|
147
|
+
server.close();
|
|
148
|
+
process.chdir(prev);
|
|
149
|
+
fail(`Error handling malformed URL: ${e.message}`);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
106
152
|
}
|
|
107
153
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
WRONG FILE
|
|
1
|
+
// WRONG FILE
|
|
@@ -1 +1 @@
|
|
|
1
|
-
fallback file
|
|
1
|
+
// fallback file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
WRONG FILE
|
|
1
|
+
// WRONG FILE
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const Import = () => 'imported';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const nested = true
|
|
@@ -1 +1 @@
|
|
|
1
|
-
single level
|
|
1
|
+
// single level
|
|
@@ -1 +1 @@
|
|
|
1
|
-
nested level
|
|
1
|
+
// nested level
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const helpers = {}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
# Test Server Root
|
|
2
|
-
|
|
3
|
-
This directory contains the persistent test server structure used by unit tests instead of creating temporary files for each test run. Tests use `withTestDir()` to get a temporary copy of this structure, maintaining isolation while providing consistent base files.
|
|
4
|
-
|
|
5
|
-
## Directory Structure
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
test-server-root/
|
|
9
|
-
├── README.md # This file
|
|
10
|
-
├── .config.json # Basic config with middleware settings
|
|
11
|
-
├── index.html # Basic home page: "<h1>Home</h1>"
|
|
12
|
-
├── late.html # For rescan tests: "later"
|
|
13
|
-
├── hello.html # For CLI tests: "hello world"
|
|
14
|
-
├── a.txt # Test file: "A"
|
|
15
|
-
├── b/
|
|
16
|
-
│ └── 1.txt # Test file: "B1"
|
|
17
|
-
├── api/
|
|
18
|
-
│ ├── GET.js # Route handler returning {ok:true, params}
|
|
19
|
-
│ └── no-default.js # Route file without default export
|
|
20
|
-
├── src/
|
|
21
|
-
│ ├── file.js # Content: "single level"
|
|
22
|
-
│ ├── file.txt # Content: "custom"
|
|
23
|
-
│ ├── nested/
|
|
24
|
-
│ │ └── file.js # Content: "nested level"
|
|
25
|
-
│ ├── components/
|
|
26
|
-
│ │ └── Button.js # Content: "export default Button"
|
|
27
|
-
│ ├── utils/
|
|
28
|
-
│ │ └── helpers/
|
|
29
|
-
│ │ └── format.js # Content: "export const format = () => {}"
|
|
30
|
-
│ └── deep/
|
|
31
|
-
│ └── nested/
|
|
32
|
-
│ └── folder/
|
|
33
|
-
│ └── file.js # Content: "export const deep = true"
|
|
34
|
-
├── docs/
|
|
35
|
-
│ ├── .config.json # Config with custom routes to ../src/**
|
|
36
|
-
│ ├── api/
|
|
37
|
-
│ │ └── data.json # Content: {"source": "static"}
|
|
38
|
-
│ └── src/
|
|
39
|
-
│ ├── nested/
|
|
40
|
-
│ │ └── file.js # Content: "fallback file"
|
|
41
|
-
│ ├── components/
|
|
42
|
-
│ │ └── Button.js # Content: "WRONG FILE"
|
|
43
|
-
│ └── utils/
|
|
44
|
-
│ └── helpers/
|
|
45
|
-
│ └── format.js # Content: "WRONG FILE"
|
|
46
|
-
├── custom/
|
|
47
|
-
│ └── data.json # Content: {"source": "custom"}
|
|
48
|
-
└── public/
|
|
49
|
-
└── src/
|
|
50
|
-
└── file.txt # Content: "static"
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## Configuration Files
|
|
54
|
-
|
|
55
|
-
### Root .config.json
|
|
56
|
-
Basic configuration for middleware testing:
|
|
57
|
-
```json
|
|
58
|
-
{
|
|
59
|
-
"middleware": {
|
|
60
|
-
"cors": {}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### docs/.config.json
|
|
66
|
-
Configuration for wildcard routing tests:
|
|
67
|
-
```json
|
|
68
|
-
{
|
|
69
|
-
"customRoutes": {
|
|
70
|
-
"/src/*": "../src/$1"
|
|
71
|
-
},
|
|
72
|
-
"routePreference": "customRoute"
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Usage in Tests
|
|
77
|
-
|
|
78
|
-
### Simple Tests
|
|
79
|
-
Use `withTestDir()` for tests that can leverage the existing structure:
|
|
80
|
-
|
|
81
|
-
```javascript
|
|
82
|
-
await withTestDir(async (dir) => {
|
|
83
|
-
// dir contains a copy of all the above files
|
|
84
|
-
// Use existing files or create additional ones as needed
|
|
85
|
-
const handler = await router({root: dir}, log);
|
|
86
|
-
// ... test the handler
|
|
87
|
-
});
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### Subdirectory Tests
|
|
91
|
-
Use the `subdir` option to work within a specific folder:
|
|
92
|
-
|
|
93
|
-
```javascript
|
|
94
|
-
await withTestDir(async (dir) => {
|
|
95
|
-
// dir points to test-server-root/docs/
|
|
96
|
-
process.chdir(dir);
|
|
97
|
-
const handler = await router({root: '.'}, log);
|
|
98
|
-
// ... test from docs folder perspective
|
|
99
|
-
}, {subdir: 'docs'});
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Complex Tests
|
|
103
|
-
For tests requiring very specific or complex structures, continue using `withTempDir()`:
|
|
104
|
-
|
|
105
|
-
```javascript
|
|
106
|
-
await withTempDir(async (dir) => {
|
|
107
|
-
await write(dir, 'very/specific/structure.js', 'content');
|
|
108
|
-
// ... custom test setup
|
|
109
|
-
});
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
## Benefits
|
|
113
|
-
|
|
114
|
-
1. **Consistency** - All tests start with the same base structure
|
|
115
|
-
2. **Speed** - No need to recreate common files for each test
|
|
116
|
-
3. **Maintainability** - Centralized test file management
|
|
117
|
-
4. **Documentation** - Clear visibility of what test files exist
|
|
118
|
-
5. **Isolation** - Each test still gets its own temporary copy
|