kempo-server 1.4.3 → 1.4.5
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/.github/copilot-instructions.md +96 -96
- package/.github/workflows/publish-npm.yml +41 -0
- package/README.md +650 -650
- package/builtinMiddleware.js +136 -136
- package/defaultConfig.js +129 -129
- package/docs/.config.json +5 -5
- package/docs/.config.json.example +19 -19
- package/docs/api/user/[id]/GET.js +15 -15
- package/docs/api/user/[id]/[info]/DELETE.js +12 -12
- package/docs/api/user/[id]/[info]/GET.js +17 -17
- package/docs/api/user/[id]/[info]/POST.js +18 -18
- package/docs/api/user/[id]/[info]/PUT.js +19 -19
- package/docs/configuration.html +119 -119
- package/docs/examples.html +201 -201
- package/docs/getting-started.html +72 -72
- package/docs/index.html +81 -81
- package/docs/manifest.json +87 -87
- package/docs/middleware.html +147 -147
- package/docs/request-response.html +95 -95
- package/docs/routing.html +77 -77
- package/example-middleware.js +23 -23
- package/example.config.json +50 -50
- package/findFile.js +138 -138
- package/getFiles.js +72 -72
- package/getFlags.js +34 -34
- package/index.js +47 -47
- package/middlewareRunner.js +25 -25
- package/package.json +10 -6
- package/requestWrapper.js +87 -87
- package/responseWrapper.js +204 -204
- package/router.js +285 -285
- package/serveFile.js +71 -71
- package/tests/builtinMiddleware-cors.node-test.js +17 -17
- package/tests/builtinMiddleware.node-test.js +74 -74
- package/tests/defaultConfig.node-test.js +13 -13
- package/tests/example-middleware.node-test.js +31 -31
- package/tests/findFile.node-test.js +46 -46
- package/tests/getFiles.node-test.js +25 -25
- package/tests/getFlags.node-test.js +30 -30
- package/tests/index.node-test.js +23 -23
- package/tests/middlewareRunner.node-test.js +18 -18
- package/tests/requestWrapper.node-test.js +51 -51
- package/tests/responseWrapper.node-test.js +74 -74
- package/tests/router-middleware.node-test.js +46 -46
- package/tests/router.node-test.js +88 -88
- package/tests/serveFile.node-test.js +52 -52
- package/tests/test-utils.js +106 -106
package/serveFile.js
CHANGED
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import { readFile } from 'fs/promises';
|
|
3
|
-
import { pathToFileURL } from 'url';
|
|
4
|
-
import findFile from './findFile.js';
|
|
5
|
-
import createRequestWrapper from './requestWrapper.js';
|
|
6
|
-
import createResponseWrapper from './responseWrapper.js';
|
|
7
|
-
|
|
8
|
-
export default async (files, rootPath, requestPath, method, config, req, res, log) => {
|
|
9
|
-
log(`Attempting to serve: ${requestPath}`, 3);
|
|
10
|
-
const [file, params] = await findFile(files, rootPath, requestPath, method, log);
|
|
11
|
-
|
|
12
|
-
if (!file) {
|
|
13
|
-
log(`No file found for: ${requestPath}`, 3);
|
|
14
|
-
return false; // Could not find file
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const fileName = path.basename(file);
|
|
18
|
-
log(`Found file: ${file}`, 2);
|
|
19
|
-
|
|
20
|
-
// Check if this is a route file that should be executed as a module
|
|
21
|
-
if (config.routeFiles.includes(fileName)) {
|
|
22
|
-
log(`Executing route file: ${fileName}`, 2);
|
|
23
|
-
try {
|
|
24
|
-
// Load the file as a module
|
|
25
|
-
const fileUrl = pathToFileURL(file).href;
|
|
26
|
-
log(`Loading module from: ${fileUrl}`, 3);
|
|
27
|
-
const module = await import(fileUrl);
|
|
28
|
-
|
|
29
|
-
// Execute the default export function
|
|
30
|
-
if (typeof module.default === 'function') {
|
|
31
|
-
log(`Executing route function with params: ${JSON.stringify(params)}`, 3);
|
|
32
|
-
|
|
33
|
-
// Create enhanced request and response wrappers
|
|
34
|
-
const enhancedRequest = createRequestWrapper(req, params);
|
|
35
|
-
const enhancedResponse = createResponseWrapper(res);
|
|
36
|
-
|
|
37
|
-
await module.default(enhancedRequest, enhancedResponse);
|
|
38
|
-
log(`Route executed successfully: ${fileName}`, 2);
|
|
39
|
-
return true; // Successfully served
|
|
40
|
-
} else {
|
|
41
|
-
log(`Route file does not export a function: ${fileName}`, 0);
|
|
42
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
43
|
-
res.end('Route file does not export a function');
|
|
44
|
-
return true; // Handled (even though it's an error)
|
|
45
|
-
}
|
|
46
|
-
} catch (error) {
|
|
47
|
-
log(`Error loading route file ${fileName}: ${error.message}`, 0);
|
|
48
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
49
|
-
res.end('Internal Server Error');
|
|
50
|
-
return true; // Handled (even though it's an error)
|
|
51
|
-
}
|
|
52
|
-
} else {
|
|
53
|
-
// Serve the file content with appropriate MIME type
|
|
54
|
-
log(`Serving static file: ${fileName}`, 2);
|
|
55
|
-
try {
|
|
56
|
-
const fileContent = await readFile(file);
|
|
57
|
-
const fileExtension = path.extname(file).toLowerCase().slice(1);
|
|
58
|
-
const mimeType = config.allowedMimes[fileExtension] || 'application/octet-stream';
|
|
59
|
-
|
|
60
|
-
log(`Serving ${file} as ${mimeType} (${fileContent.length} bytes)`, 2);
|
|
61
|
-
res.writeHead(200, { 'Content-Type': mimeType });
|
|
62
|
-
res.end(fileContent);
|
|
63
|
-
return true; // Successfully served
|
|
64
|
-
} catch (error) {
|
|
65
|
-
log(`Error reading file ${file}: ${error.message}`, 0);
|
|
66
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
67
|
-
res.end('Internal Server Error');
|
|
68
|
-
return true; // Handled (even though it's an error)
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
};
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import findFile from './findFile.js';
|
|
5
|
+
import createRequestWrapper from './requestWrapper.js';
|
|
6
|
+
import createResponseWrapper from './responseWrapper.js';
|
|
7
|
+
|
|
8
|
+
export default async (files, rootPath, requestPath, method, config, req, res, log) => {
|
|
9
|
+
log(`Attempting to serve: ${requestPath}`, 3);
|
|
10
|
+
const [file, params] = await findFile(files, rootPath, requestPath, method, log);
|
|
11
|
+
|
|
12
|
+
if (!file) {
|
|
13
|
+
log(`No file found for: ${requestPath}`, 3);
|
|
14
|
+
return false; // Could not find file
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const fileName = path.basename(file);
|
|
18
|
+
log(`Found file: ${file}`, 2);
|
|
19
|
+
|
|
20
|
+
// Check if this is a route file that should be executed as a module
|
|
21
|
+
if (config.routeFiles.includes(fileName)) {
|
|
22
|
+
log(`Executing route file: ${fileName}`, 2);
|
|
23
|
+
try {
|
|
24
|
+
// Load the file as a module
|
|
25
|
+
const fileUrl = pathToFileURL(file).href;
|
|
26
|
+
log(`Loading module from: ${fileUrl}`, 3);
|
|
27
|
+
const module = await import(fileUrl);
|
|
28
|
+
|
|
29
|
+
// Execute the default export function
|
|
30
|
+
if (typeof module.default === 'function') {
|
|
31
|
+
log(`Executing route function with params: ${JSON.stringify(params)}`, 3);
|
|
32
|
+
|
|
33
|
+
// Create enhanced request and response wrappers
|
|
34
|
+
const enhancedRequest = createRequestWrapper(req, params);
|
|
35
|
+
const enhancedResponse = createResponseWrapper(res);
|
|
36
|
+
|
|
37
|
+
await module.default(enhancedRequest, enhancedResponse);
|
|
38
|
+
log(`Route executed successfully: ${fileName}`, 2);
|
|
39
|
+
return true; // Successfully served
|
|
40
|
+
} else {
|
|
41
|
+
log(`Route file does not export a function: ${fileName}`, 0);
|
|
42
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
43
|
+
res.end('Route file does not export a function');
|
|
44
|
+
return true; // Handled (even though it's an error)
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
log(`Error loading route file ${fileName}: ${error.message}`, 0);
|
|
48
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
49
|
+
res.end('Internal Server Error');
|
|
50
|
+
return true; // Handled (even though it's an error)
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
// Serve the file content with appropriate MIME type
|
|
54
|
+
log(`Serving static file: ${fileName}`, 2);
|
|
55
|
+
try {
|
|
56
|
+
const fileContent = await readFile(file);
|
|
57
|
+
const fileExtension = path.extname(file).toLowerCase().slice(1);
|
|
58
|
+
const mimeType = config.allowedMimes[fileExtension] || 'application/octet-stream';
|
|
59
|
+
|
|
60
|
+
log(`Serving ${file} as ${mimeType} (${fileContent.length} bytes)`, 2);
|
|
61
|
+
res.writeHead(200, { 'Content-Type': mimeType });
|
|
62
|
+
res.end(fileContent);
|
|
63
|
+
return true; // Successfully served
|
|
64
|
+
} catch (error) {
|
|
65
|
+
log(`Error reading file ${file}: ${error.message}`, 0);
|
|
66
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
67
|
+
res.end('Internal Server Error');
|
|
68
|
+
return true; // Handled (even though it's an error)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import {corsMiddleware} from '../builtinMiddleware.js';
|
|
2
|
-
import {createMockReq, createMockRes, expect} from './test-utils.js';
|
|
3
|
-
|
|
4
|
-
export default {
|
|
5
|
-
'cors origin array and non-OPTIONS continues to next': async ({pass, fail}) => {
|
|
6
|
-
try {
|
|
7
|
-
const res = createMockRes();
|
|
8
|
-
const mw = corsMiddleware({origin: ['http://a','http://b'], methods: ['GET'], headers: ['X']});
|
|
9
|
-
const req = createMockReq({method: 'GET', headers: {origin: 'http://b'}});
|
|
10
|
-
let called = false;
|
|
11
|
-
await mw(req, res, async () => { called = true; });
|
|
12
|
-
expect(called, 'next not called');
|
|
13
|
-
expect(res.getHeader('Access-Control-Allow-Origin') === 'http://b', 'allowed origin');
|
|
14
|
-
pass('cors array');
|
|
15
|
-
} catch(e){ fail(e.message); }
|
|
16
|
-
}
|
|
17
|
-
};
|
|
1
|
+
import {corsMiddleware} from '../builtinMiddleware.js';
|
|
2
|
+
import {createMockReq, createMockRes, expect} from './test-utils.js';
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
'cors origin array and non-OPTIONS continues to next': async ({pass, fail}) => {
|
|
6
|
+
try {
|
|
7
|
+
const res = createMockRes();
|
|
8
|
+
const mw = corsMiddleware({origin: ['http://a','http://b'], methods: ['GET'], headers: ['X']});
|
|
9
|
+
const req = createMockReq({method: 'GET', headers: {origin: 'http://b'}});
|
|
10
|
+
let called = false;
|
|
11
|
+
await mw(req, res, async () => { called = true; });
|
|
12
|
+
expect(called, 'next not called');
|
|
13
|
+
expect(res.getHeader('Access-Control-Allow-Origin') === 'http://b', 'allowed origin');
|
|
14
|
+
pass('cors array');
|
|
15
|
+
} catch(e){ fail(e.message); }
|
|
16
|
+
}
|
|
17
|
+
};
|
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
import http from 'http';
|
|
2
|
-
import {createMockReq, createMockRes, expect, bigString, gzipSize, setEnv} from './test-utils.js';
|
|
3
|
-
import {corsMiddleware, compressionMiddleware, rateLimitMiddleware, securityMiddleware, loggingMiddleware} from '../builtinMiddleware.js';
|
|
4
|
-
|
|
5
|
-
export default {
|
|
6
|
-
'cors middleware sets headers and handles OPTIONS': async ({pass, fail}) => {
|
|
7
|
-
try {
|
|
8
|
-
const res = createMockRes();
|
|
9
|
-
const mw = corsMiddleware({origin: '*', methods: ['GET'], headers: ['X']});
|
|
10
|
-
const req = createMockReq({method: 'OPTIONS', headers: {origin: 'http://x'}});
|
|
11
|
-
await mw(req, res, async () => {});
|
|
12
|
-
expect(res.isEnded(), 'preflight should end');
|
|
13
|
-
expect(res.getHeader('Access-Control-Allow-Origin') === 'http://x' || res.getHeader('Access-Control-Allow-Origin') === '*', 'origin header');
|
|
14
|
-
pass('cors');
|
|
15
|
-
} catch(e){ fail(e.message); }
|
|
16
|
-
},
|
|
17
|
-
'compression middleware gzips when threshold met and client accepts': async ({pass, fail}) => {
|
|
18
|
-
try {
|
|
19
|
-
const res = createMockRes();
|
|
20
|
-
const mw = compressionMiddleware({threshold: 1024});
|
|
21
|
-
const req = createMockReq({headers: {'accept-encoding': 'gzip'}});
|
|
22
|
-
// simulate next writing large body
|
|
23
|
-
await mw(req, res, async () => {
|
|
24
|
-
res.write(bigString(5000));
|
|
25
|
-
res.end();
|
|
26
|
-
});
|
|
27
|
-
await new Promise(r => setTimeout(r, 10));
|
|
28
|
-
const body = res.getBody();
|
|
29
|
-
const original = Buffer.from(bigString(5000));
|
|
30
|
-
const gzLen = await gzipSize(original);
|
|
31
|
-
// If gzipped is smaller, we expect gzip header. Otherwise, implementation may send uncompressed.
|
|
32
|
-
if(gzLen < original.length){
|
|
33
|
-
expect(res.getHeader('Content-Encoding') === 'gzip', 'should gzip when beneficial');
|
|
34
|
-
}
|
|
35
|
-
expect(body.length > 0, 'has body');
|
|
36
|
-
pass('compression');
|
|
37
|
-
} catch(e){ fail(e.message); }
|
|
38
|
-
},
|
|
39
|
-
'rate limit returns 429 after maxRequests': async ({pass, fail}) => {
|
|
40
|
-
try {
|
|
41
|
-
const cfg = {maxRequests: 2, windowMs: 1000, message: 'Too many'};
|
|
42
|
-
const mw = rateLimitMiddleware(cfg);
|
|
43
|
-
const req = createMockReq();
|
|
44
|
-
const res1 = createMockRes();
|
|
45
|
-
await mw(req, res1, async () => {});
|
|
46
|
-
const res2 = createMockRes();
|
|
47
|
-
await mw(req, res2, async () => {});
|
|
48
|
-
const res3 = createMockRes();
|
|
49
|
-
await mw(req, res3, async () => {});
|
|
50
|
-
expect(res3.statusCode === 429, 'should rate limit');
|
|
51
|
-
pass('rateLimit');
|
|
52
|
-
} catch(e){ fail(e.message); }
|
|
53
|
-
},
|
|
54
|
-
'security middleware sets headers': async ({pass, fail}) => {
|
|
55
|
-
try {
|
|
56
|
-
const res = createMockRes();
|
|
57
|
-
const mw = securityMiddleware({headers: {'X-Test': '1'}});
|
|
58
|
-
await mw(createMockReq(), res, async () => {});
|
|
59
|
-
expect(res.getHeader('X-Test') === '1', 'header set');
|
|
60
|
-
pass('security');
|
|
61
|
-
} catch(e){ fail(e.message); }
|
|
62
|
-
},
|
|
63
|
-
'logging middleware logs after response end': async ({pass, fail}) => {
|
|
64
|
-
try {
|
|
65
|
-
const logs = [];
|
|
66
|
-
const logger = (m) => logs.push(String(m));
|
|
67
|
-
const mw = loggingMiddleware({includeUserAgent: true, includeResponseTime: true}, logger);
|
|
68
|
-
const res = createMockRes();
|
|
69
|
-
await mw(createMockReq({headers: {'user-agent': 'UA'}}), res, async () => { res.end('x'); });
|
|
70
|
-
expect(logs.length === 1 && logs[0].includes('GET /') && logs[0].includes('UA'), 'logged');
|
|
71
|
-
pass('logging');
|
|
72
|
-
} catch(e){ fail(e.message); }
|
|
73
|
-
}
|
|
74
|
-
};
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import {createMockReq, createMockRes, expect, bigString, gzipSize, setEnv} from './test-utils.js';
|
|
3
|
+
import {corsMiddleware, compressionMiddleware, rateLimitMiddleware, securityMiddleware, loggingMiddleware} from '../builtinMiddleware.js';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
'cors middleware sets headers and handles OPTIONS': async ({pass, fail}) => {
|
|
7
|
+
try {
|
|
8
|
+
const res = createMockRes();
|
|
9
|
+
const mw = corsMiddleware({origin: '*', methods: ['GET'], headers: ['X']});
|
|
10
|
+
const req = createMockReq({method: 'OPTIONS', headers: {origin: 'http://x'}});
|
|
11
|
+
await mw(req, res, async () => {});
|
|
12
|
+
expect(res.isEnded(), 'preflight should end');
|
|
13
|
+
expect(res.getHeader('Access-Control-Allow-Origin') === 'http://x' || res.getHeader('Access-Control-Allow-Origin') === '*', 'origin header');
|
|
14
|
+
pass('cors');
|
|
15
|
+
} catch(e){ fail(e.message); }
|
|
16
|
+
},
|
|
17
|
+
'compression middleware gzips when threshold met and client accepts': async ({pass, fail}) => {
|
|
18
|
+
try {
|
|
19
|
+
const res = createMockRes();
|
|
20
|
+
const mw = compressionMiddleware({threshold: 1024});
|
|
21
|
+
const req = createMockReq({headers: {'accept-encoding': 'gzip'}});
|
|
22
|
+
// simulate next writing large body
|
|
23
|
+
await mw(req, res, async () => {
|
|
24
|
+
res.write(bigString(5000));
|
|
25
|
+
res.end();
|
|
26
|
+
});
|
|
27
|
+
await new Promise(r => setTimeout(r, 10));
|
|
28
|
+
const body = res.getBody();
|
|
29
|
+
const original = Buffer.from(bigString(5000));
|
|
30
|
+
const gzLen = await gzipSize(original);
|
|
31
|
+
// If gzipped is smaller, we expect gzip header. Otherwise, implementation may send uncompressed.
|
|
32
|
+
if(gzLen < original.length){
|
|
33
|
+
expect(res.getHeader('Content-Encoding') === 'gzip', 'should gzip when beneficial');
|
|
34
|
+
}
|
|
35
|
+
expect(body.length > 0, 'has body');
|
|
36
|
+
pass('compression');
|
|
37
|
+
} catch(e){ fail(e.message); }
|
|
38
|
+
},
|
|
39
|
+
'rate limit returns 429 after maxRequests': async ({pass, fail}) => {
|
|
40
|
+
try {
|
|
41
|
+
const cfg = {maxRequests: 2, windowMs: 1000, message: 'Too many'};
|
|
42
|
+
const mw = rateLimitMiddleware(cfg);
|
|
43
|
+
const req = createMockReq();
|
|
44
|
+
const res1 = createMockRes();
|
|
45
|
+
await mw(req, res1, async () => {});
|
|
46
|
+
const res2 = createMockRes();
|
|
47
|
+
await mw(req, res2, async () => {});
|
|
48
|
+
const res3 = createMockRes();
|
|
49
|
+
await mw(req, res3, async () => {});
|
|
50
|
+
expect(res3.statusCode === 429, 'should rate limit');
|
|
51
|
+
pass('rateLimit');
|
|
52
|
+
} catch(e){ fail(e.message); }
|
|
53
|
+
},
|
|
54
|
+
'security middleware sets headers': async ({pass, fail}) => {
|
|
55
|
+
try {
|
|
56
|
+
const res = createMockRes();
|
|
57
|
+
const mw = securityMiddleware({headers: {'X-Test': '1'}});
|
|
58
|
+
await mw(createMockReq(), res, async () => {});
|
|
59
|
+
expect(res.getHeader('X-Test') === '1', 'header set');
|
|
60
|
+
pass('security');
|
|
61
|
+
} catch(e){ fail(e.message); }
|
|
62
|
+
},
|
|
63
|
+
'logging middleware logs after response end': async ({pass, fail}) => {
|
|
64
|
+
try {
|
|
65
|
+
const logs = [];
|
|
66
|
+
const logger = (m) => logs.push(String(m));
|
|
67
|
+
const mw = loggingMiddleware({includeUserAgent: true, includeResponseTime: true}, logger);
|
|
68
|
+
const res = createMockRes();
|
|
69
|
+
await mw(createMockReq({headers: {'user-agent': 'UA'}}), res, async () => { res.end('x'); });
|
|
70
|
+
expect(logs.length === 1 && logs[0].includes('GET /') && logs[0].includes('UA'), 'logged');
|
|
71
|
+
pass('logging');
|
|
72
|
+
} catch(e){ fail(e.message); }
|
|
73
|
+
}
|
|
74
|
+
};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import defaultConfig from '../defaultConfig.js';
|
|
2
|
-
|
|
3
|
-
export default {
|
|
4
|
-
'defaultConfig contains required fields and types': async ({pass, fail}) => {
|
|
5
|
-
try {
|
|
6
|
-
if(typeof defaultConfig !== 'object') throw new Error('not object');
|
|
7
|
-
if(!defaultConfig.allowedMimes || !defaultConfig.disallowedRegex) throw new Error('missing keys');
|
|
8
|
-
if(!defaultConfig.routeFiles.includes('GET.js')) throw new Error('routeFiles missing GET.js');
|
|
9
|
-
if(!defaultConfig.middleware || typeof defaultConfig.middleware !== 'object') throw new Error('middleware missing');
|
|
10
|
-
pass('shape ok');
|
|
11
|
-
} catch(e){ fail(e.message); }
|
|
12
|
-
}
|
|
13
|
-
};
|
|
1
|
+
import defaultConfig from '../defaultConfig.js';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
'defaultConfig contains required fields and types': async ({pass, fail}) => {
|
|
5
|
+
try {
|
|
6
|
+
if(typeof defaultConfig !== 'object') throw new Error('not object');
|
|
7
|
+
if(!defaultConfig.allowedMimes || !defaultConfig.disallowedRegex) throw new Error('missing keys');
|
|
8
|
+
if(!defaultConfig.routeFiles.includes('GET.js')) throw new Error('routeFiles missing GET.js');
|
|
9
|
+
if(!defaultConfig.middleware || typeof defaultConfig.middleware !== 'object') throw new Error('middleware missing');
|
|
10
|
+
pass('shape ok');
|
|
11
|
+
} catch(e){ fail(e.message); }
|
|
12
|
+
}
|
|
13
|
+
};
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import url from 'url';
|
|
3
|
-
import {createMockReq, createMockRes, expect, setEnv} from './test-utils.js';
|
|
4
|
-
|
|
5
|
-
// import the middleware module by file path to avoid executing index.js
|
|
6
|
-
const examplePath = path.join(process.cwd(), 'example-middleware.js');
|
|
7
|
-
const {default: authMiddleware} = await import(url.pathToFileURL(examplePath));
|
|
8
|
-
|
|
9
|
-
export default {
|
|
10
|
-
'blocks when API key missing and allows when present': async ({pass, fail}) => {
|
|
11
|
-
try {
|
|
12
|
-
await setEnv({API_KEY: 'abc'}, async () => {
|
|
13
|
-
const res1 = createMockRes();
|
|
14
|
-
await authMiddleware(createMockReq({url:'/private'}), res1, async ()=>{});
|
|
15
|
-
expect(res1.statusCode === 401, 'should 401 without key');
|
|
16
|
-
|
|
17
|
-
const res2 = createMockRes();
|
|
18
|
-
const req2 = createMockReq({headers: {'x-api-key': 'abc'}, url:'/private'});
|
|
19
|
-
let called = false;
|
|
20
|
-
await authMiddleware(req2, res2, async ()=>{ called = true; });
|
|
21
|
-
expect(called, 'should call next');
|
|
22
|
-
expect(req2.user && req2.user.authenticated, 'user attached');
|
|
23
|
-
|
|
24
|
-
const res3 = createMockRes();
|
|
25
|
-
await authMiddleware(createMockReq({url:'/public/file'}), res3, async ()=>{});
|
|
26
|
-
expect(res3.isEnded() === false, 'public should not end');
|
|
27
|
-
});
|
|
28
|
-
pass('auth middleware');
|
|
29
|
-
} catch(e){ fail(e.message); }
|
|
30
|
-
}
|
|
31
|
-
};
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import url from 'url';
|
|
3
|
+
import {createMockReq, createMockRes, expect, setEnv} from './test-utils.js';
|
|
4
|
+
|
|
5
|
+
// import the middleware module by file path to avoid executing index.js
|
|
6
|
+
const examplePath = path.join(process.cwd(), 'example-middleware.js');
|
|
7
|
+
const {default: authMiddleware} = await import(url.pathToFileURL(examplePath));
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
'blocks when API key missing and allows when present': async ({pass, fail}) => {
|
|
11
|
+
try {
|
|
12
|
+
await setEnv({API_KEY: 'abc'}, async () => {
|
|
13
|
+
const res1 = createMockRes();
|
|
14
|
+
await authMiddleware(createMockReq({url:'/private'}), res1, async ()=>{});
|
|
15
|
+
expect(res1.statusCode === 401, 'should 401 without key');
|
|
16
|
+
|
|
17
|
+
const res2 = createMockRes();
|
|
18
|
+
const req2 = createMockReq({headers: {'x-api-key': 'abc'}, url:'/private'});
|
|
19
|
+
let called = false;
|
|
20
|
+
await authMiddleware(req2, res2, async ()=>{ called = true; });
|
|
21
|
+
expect(called, 'should call next');
|
|
22
|
+
expect(req2.user && req2.user.authenticated, 'user attached');
|
|
23
|
+
|
|
24
|
+
const res3 = createMockRes();
|
|
25
|
+
await authMiddleware(createMockReq({url:'/public/file'}), res3, async ()=>{});
|
|
26
|
+
expect(res3.isEnded() === false, 'public should not end');
|
|
27
|
+
});
|
|
28
|
+
pass('auth middleware');
|
|
29
|
+
} catch(e){ fail(e.message); }
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import findFile from '../findFile.js';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
const toAbs = (root, p) => path.join(root, p);
|
|
5
|
-
|
|
6
|
-
export default {
|
|
7
|
-
'exact match returns file': async ({pass, fail, log}) => {
|
|
8
|
-
try {
|
|
9
|
-
const root = path.join(process.cwd(), 'tmp-root');
|
|
10
|
-
const files = [toAbs(root, 'a/b/GET.js')];
|
|
11
|
-
const [file, params] = await findFile(files, root, '/a/b/GET.js', 'GET', log);
|
|
12
|
-
if(file !== files[0]) throw new Error('not exact');
|
|
13
|
-
if(Object.keys(params).length !== 0) throw new Error('params present');
|
|
14
|
-
pass('exact');
|
|
15
|
-
} catch(e){ fail(e.message); }
|
|
16
|
-
},
|
|
17
|
-
'directory index prioritization and method specific': async ({pass, fail, log}) => {
|
|
18
|
-
try {
|
|
19
|
-
const root = path.join(process.cwd(), 'tmp-root');
|
|
20
|
-
const files = ['a/index.html', 'a/GET.js', 'a/index.js'].map(p => toAbs(root, p));
|
|
21
|
-
const [file] = await findFile(files, root, '/a', 'GET', log);
|
|
22
|
-
if(!file || path.basename(file) !== 'GET.js') throw new Error('priority not respected');
|
|
23
|
-
pass('dir index');
|
|
24
|
-
} catch(e){ fail(e.message); }
|
|
25
|
-
},
|
|
26
|
-
'dynamic match with params and best priority': async ({pass, fail, log}) => {
|
|
27
|
-
try {
|
|
28
|
-
const root = path.join(process.cwd(), 'tmp-root');
|
|
29
|
-
const files = ['user/[id]/GET.js', 'user/[id]/index.html', 'user/[id]/index.js'].map(p => toAbs(root, p));
|
|
30
|
-
const [file, params] = await findFile(files, root, '/user/42', 'GET', log);
|
|
31
|
-
if(!file || path.basename(file) !== 'GET.js') throw new Error('did not pick GET.js');
|
|
32
|
-
if(params.id !== '42') throw new Error('param missing');
|
|
33
|
-
pass('dynamic');
|
|
34
|
-
} catch(e){ fail(e.message); }
|
|
35
|
-
},
|
|
36
|
-
'no match returns false and empty params': async ({pass, fail, log}) => {
|
|
37
|
-
try {
|
|
38
|
-
const root = path.join(process.cwd(), 'tmp-root');
|
|
39
|
-
const files = ['x/y/index.html'].map(p => toAbs(root, p));
|
|
40
|
-
const [file, params] = await findFile(files, root, '/nope', 'GET', log);
|
|
41
|
-
if(file !== false) throw new Error('should be false');
|
|
42
|
-
if(Object.keys(params).length !== 0) throw new Error('params not empty');
|
|
43
|
-
pass('no match');
|
|
44
|
-
} catch(e){ fail(e.message); }
|
|
45
|
-
}
|
|
46
|
-
};
|
|
1
|
+
import findFile from '../findFile.js';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const toAbs = (root, p) => path.join(root, p);
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
'exact match returns file': async ({pass, fail, log}) => {
|
|
8
|
+
try {
|
|
9
|
+
const root = path.join(process.cwd(), 'tmp-root');
|
|
10
|
+
const files = [toAbs(root, 'a/b/GET.js')];
|
|
11
|
+
const [file, params] = await findFile(files, root, '/a/b/GET.js', 'GET', log);
|
|
12
|
+
if(file !== files[0]) throw new Error('not exact');
|
|
13
|
+
if(Object.keys(params).length !== 0) throw new Error('params present');
|
|
14
|
+
pass('exact');
|
|
15
|
+
} catch(e){ fail(e.message); }
|
|
16
|
+
},
|
|
17
|
+
'directory index prioritization and method specific': async ({pass, fail, log}) => {
|
|
18
|
+
try {
|
|
19
|
+
const root = path.join(process.cwd(), 'tmp-root');
|
|
20
|
+
const files = ['a/index.html', 'a/GET.js', 'a/index.js'].map(p => toAbs(root, p));
|
|
21
|
+
const [file] = await findFile(files, root, '/a', 'GET', log);
|
|
22
|
+
if(!file || path.basename(file) !== 'GET.js') throw new Error('priority not respected');
|
|
23
|
+
pass('dir index');
|
|
24
|
+
} catch(e){ fail(e.message); }
|
|
25
|
+
},
|
|
26
|
+
'dynamic match with params and best priority': async ({pass, fail, log}) => {
|
|
27
|
+
try {
|
|
28
|
+
const root = path.join(process.cwd(), 'tmp-root');
|
|
29
|
+
const files = ['user/[id]/GET.js', 'user/[id]/index.html', 'user/[id]/index.js'].map(p => toAbs(root, p));
|
|
30
|
+
const [file, params] = await findFile(files, root, '/user/42', 'GET', log);
|
|
31
|
+
if(!file || path.basename(file) !== 'GET.js') throw new Error('did not pick GET.js');
|
|
32
|
+
if(params.id !== '42') throw new Error('param missing');
|
|
33
|
+
pass('dynamic');
|
|
34
|
+
} catch(e){ fail(e.message); }
|
|
35
|
+
},
|
|
36
|
+
'no match returns false and empty params': async ({pass, fail, log}) => {
|
|
37
|
+
try {
|
|
38
|
+
const root = path.join(process.cwd(), 'tmp-root');
|
|
39
|
+
const files = ['x/y/index.html'].map(p => toAbs(root, p));
|
|
40
|
+
const [file, params] = await findFile(files, root, '/nope', 'GET', log);
|
|
41
|
+
if(file !== false) throw new Error('should be false');
|
|
42
|
+
if(Object.keys(params).length !== 0) throw new Error('params not empty');
|
|
43
|
+
pass('no match');
|
|
44
|
+
} catch(e){ fail(e.message); }
|
|
45
|
+
}
|
|
46
|
+
};
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import getFiles from '../getFiles.js';
|
|
2
|
-
import defaultConfig from '../defaultConfig.js';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import {withTempDir, write, expect, log} from './test-utils.js';
|
|
5
|
-
|
|
6
|
-
export default {
|
|
7
|
-
'scans directories recursively and filters by mime and disallowed': async ({pass, fail}) => {
|
|
8
|
-
try {
|
|
9
|
-
await withTempDir(async (dir) => {
|
|
10
|
-
const cfg = JSON.parse(JSON.stringify(defaultConfig));
|
|
11
|
-
await write(dir, 'index.html', '<!doctype html>');
|
|
12
|
-
await write(dir, '.env', 'SECRET=1');
|
|
13
|
-
await write(dir, 'notes.xyz', 'unknown');
|
|
14
|
-
await write(dir, 'sub/app.js', 'console.log(1)');
|
|
15
|
-
const files = await getFiles(dir, cfg, log);
|
|
16
|
-
const rel = files.map(f => path.relative(dir, f).replace(/\\/g, '/'));
|
|
17
|
-
expect(rel.includes('index.html'), 'includes html');
|
|
18
|
-
expect(rel.includes('sub/app.js'), 'includes js');
|
|
19
|
-
expect(!rel.includes('.env'), 'excludes disallowed');
|
|
20
|
-
expect(!rel.includes('notes.xyz'), 'excludes unknown ext');
|
|
21
|
-
});
|
|
22
|
-
pass('scan and filter');
|
|
23
|
-
} catch(e){ fail(e.message); }
|
|
24
|
-
}
|
|
25
|
-
};
|
|
1
|
+
import getFiles from '../getFiles.js';
|
|
2
|
+
import defaultConfig from '../defaultConfig.js';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import {withTempDir, write, expect, log} from './test-utils.js';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
'scans directories recursively and filters by mime and disallowed': async ({pass, fail}) => {
|
|
8
|
+
try {
|
|
9
|
+
await withTempDir(async (dir) => {
|
|
10
|
+
const cfg = JSON.parse(JSON.stringify(defaultConfig));
|
|
11
|
+
await write(dir, 'index.html', '<!doctype html>');
|
|
12
|
+
await write(dir, '.env', 'SECRET=1');
|
|
13
|
+
await write(dir, 'notes.xyz', 'unknown');
|
|
14
|
+
await write(dir, 'sub/app.js', 'console.log(1)');
|
|
15
|
+
const files = await getFiles(dir, cfg, log);
|
|
16
|
+
const rel = files.map(f => path.relative(dir, f).replace(/\\/g, '/'));
|
|
17
|
+
expect(rel.includes('index.html'), 'includes html');
|
|
18
|
+
expect(rel.includes('sub/app.js'), 'includes js');
|
|
19
|
+
expect(!rel.includes('.env'), 'excludes disallowed');
|
|
20
|
+
expect(!rel.includes('notes.xyz'), 'excludes unknown ext');
|
|
21
|
+
});
|
|
22
|
+
pass('scan and filter');
|
|
23
|
+
} catch(e){ fail(e.message); }
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import getFlags from '../getFlags.js';
|
|
2
|
-
|
|
3
|
-
export default {
|
|
4
|
-
'parses long flags with values and booleans': async ({pass, fail}) => {
|
|
5
|
-
const args = ['--port', '8080', '--scan'];
|
|
6
|
-
const flags = getFlags(args, {port: 3000, scan: false});
|
|
7
|
-
try {
|
|
8
|
-
if(flags.port !== '8080') throw new Error('port not parsed');
|
|
9
|
-
if(flags.scan !== true) throw new Error('scan boolean not parsed');
|
|
10
|
-
pass('parsed long flags');
|
|
11
|
-
} catch(e){ fail(e.message); }
|
|
12
|
-
},
|
|
13
|
-
'parses short flags using map and preserves defaults': async ({pass, fail}) => {
|
|
14
|
-
const args = ['-p', '9090', '-s'];
|
|
15
|
-
const flags = getFlags(args, {port: 3000, scan: false}, {p: 'port', s: 'scan'});
|
|
16
|
-
try {
|
|
17
|
-
if(flags.port !== '9090') throw new Error('short mapped value failed');
|
|
18
|
-
if(flags.scan !== true) throw new Error('short mapped boolean failed');
|
|
19
|
-
pass('short flags parsed');
|
|
20
|
-
} catch(e){ fail(e.message); }
|
|
21
|
-
},
|
|
22
|
-
'treats next arg starting with dash as boolean flag': async ({pass, fail}) => {
|
|
23
|
-
const args = ['-l', '-5', 'file'];
|
|
24
|
-
const flags = getFlags(args, {l: 2});
|
|
25
|
-
try {
|
|
26
|
-
if(flags.l !== true) throw new Error('should be boolean true');
|
|
27
|
-
pass('dash after flag -> boolean');
|
|
28
|
-
} catch(e){ fail(e.message); }
|
|
29
|
-
}
|
|
30
|
-
};
|
|
1
|
+
import getFlags from '../getFlags.js';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
'parses long flags with values and booleans': async ({pass, fail}) => {
|
|
5
|
+
const args = ['--port', '8080', '--scan'];
|
|
6
|
+
const flags = getFlags(args, {port: 3000, scan: false});
|
|
7
|
+
try {
|
|
8
|
+
if(flags.port !== '8080') throw new Error('port not parsed');
|
|
9
|
+
if(flags.scan !== true) throw new Error('scan boolean not parsed');
|
|
10
|
+
pass('parsed long flags');
|
|
11
|
+
} catch(e){ fail(e.message); }
|
|
12
|
+
},
|
|
13
|
+
'parses short flags using map and preserves defaults': async ({pass, fail}) => {
|
|
14
|
+
const args = ['-p', '9090', '-s'];
|
|
15
|
+
const flags = getFlags(args, {port: 3000, scan: false}, {p: 'port', s: 'scan'});
|
|
16
|
+
try {
|
|
17
|
+
if(flags.port !== '9090') throw new Error('short mapped value failed');
|
|
18
|
+
if(flags.scan !== true) throw new Error('short mapped boolean failed');
|
|
19
|
+
pass('short flags parsed');
|
|
20
|
+
} catch(e){ fail(e.message); }
|
|
21
|
+
},
|
|
22
|
+
'treats next arg starting with dash as boolean flag': async ({pass, fail}) => {
|
|
23
|
+
const args = ['-l', '-5', 'file'];
|
|
24
|
+
const flags = getFlags(args, {l: 2});
|
|
25
|
+
try {
|
|
26
|
+
if(flags.l !== true) throw new Error('should be boolean true');
|
|
27
|
+
pass('dash after flag -> boolean');
|
|
28
|
+
} catch(e){ fail(e.message); }
|
|
29
|
+
}
|
|
30
|
+
};
|