kempo-server 1.6.0 → 1.6.2
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/README.md +118 -0
- package/config-examples/development.config.json +24 -0
- package/config-examples/low-memory.config.json +23 -0
- package/config-examples/no-cache.config.json +13 -0
- package/config-examples/production.config.json +38 -0
- package/docs/.config.json.example +11 -1
- package/docs/api/_admin/cache/DELETE.js +28 -0
- package/docs/api/_admin/cache/GET.js +53 -0
- package/docs/caching.html +235 -0
- package/docs/configuration.html +76 -0
- package/docs/index.html +1 -0
- package/example-cache.config.json +45 -0
- package/package.json +1 -1
- package/src/defaultConfig.js +10 -0
- package/src/moduleCache.js +270 -0
- package/src/router.js +28 -4
- package/src/serveFile.js +35 -6
- package/tests/builtinMiddleware-cors.node-test.js +10 -10
- package/tests/builtinMiddleware.node-test.js +57 -58
- package/tests/cacheConfig.node-test.js +117 -0
- package/tests/defaultConfig.node-test.js +6 -7
- package/tests/example-middleware.node-test.js +16 -17
- package/tests/findFile.node-test.js +35 -35
- package/tests/getFiles.node-test.js +16 -16
- package/tests/getFlags.node-test.js +14 -14
- package/tests/index.node-test.js +24 -16
- package/tests/middlewareRunner.node-test.js +11 -11
- package/tests/moduleCache.node-test.js +279 -0
- package/tests/requestWrapper.node-test.js +28 -29
- package/tests/responseWrapper.node-test.js +55 -55
- package/tests/router-middleware.node-test.js +49 -37
- package/tests/router.node-test.js +96 -76
- package/utils/cache-demo.js +145 -0
- package/utils/cache-monitor.js +132 -0
|
@@ -4,71 +4,70 @@ import {corsMiddleware, compressionMiddleware, rateLimitMiddleware, securityMidd
|
|
|
4
4
|
|
|
5
5
|
export default {
|
|
6
6
|
'cors middleware sets headers and handles OPTIONS': async ({pass, fail}) => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
const res = createMockRes();
|
|
8
|
+
const mw = corsMiddleware({origin: '*', methods: ['GET'], headers: ['X']});
|
|
9
|
+
const req = createMockReq({method: 'OPTIONS', headers: {origin: 'http://x'}});
|
|
10
|
+
await mw(req, res, async () => {});
|
|
11
|
+
|
|
12
|
+
if(!res.isEnded()) return fail('preflight should end');
|
|
13
|
+
if(!(res.getHeader('Access-Control-Allow-Origin') === 'http://x' || res.getHeader('Access-Control-Allow-Origin') === '*')) return fail('origin header');
|
|
14
|
+
|
|
15
|
+
pass('cors');
|
|
16
16
|
},
|
|
17
17
|
'compression middleware gzips when threshold met and client accepts': async ({pass, fail}) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
} catch(e){ fail(e.message); }
|
|
18
|
+
const res = createMockRes();
|
|
19
|
+
const mw = compressionMiddleware({threshold: 1024});
|
|
20
|
+
const req = createMockReq({headers: {'accept-encoding': 'gzip'}});
|
|
21
|
+
// simulate next writing large body
|
|
22
|
+
await mw(req, res, async () => {
|
|
23
|
+
res.write(bigString(5000));
|
|
24
|
+
res.end();
|
|
25
|
+
});
|
|
26
|
+
await new Promise(r => setTimeout(r, 10));
|
|
27
|
+
const body = res.getBody();
|
|
28
|
+
const original = Buffer.from(bigString(5000));
|
|
29
|
+
const gzLen = await gzipSize(original);
|
|
30
|
+
// If gzipped is smaller, we expect gzip header. Otherwise, implementation may send uncompressed.
|
|
31
|
+
if(gzLen < original.length){
|
|
32
|
+
if(res.getHeader('Content-Encoding') !== 'gzip') return fail('should gzip when beneficial');
|
|
33
|
+
}
|
|
34
|
+
if(body.length <= 0) return fail('has body');
|
|
35
|
+
|
|
36
|
+
pass('compression');
|
|
38
37
|
},
|
|
39
38
|
'rate limit returns 429 after maxRequests': async ({pass, fail}) => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
const cfg = {maxRequests: 2, windowMs: 1000, message: 'Too many'};
|
|
40
|
+
const mw = rateLimitMiddleware(cfg);
|
|
41
|
+
const req = createMockReq();
|
|
42
|
+
const res1 = createMockRes();
|
|
43
|
+
await mw(req, res1, async () => {});
|
|
44
|
+
const res2 = createMockRes();
|
|
45
|
+
await mw(req, res2, async () => {});
|
|
46
|
+
const res3 = createMockRes();
|
|
47
|
+
await mw(req, res3, async () => {});
|
|
48
|
+
|
|
49
|
+
if(res3.statusCode !== 429) return fail('should rate limit');
|
|
50
|
+
|
|
51
|
+
pass('rateLimit');
|
|
53
52
|
},
|
|
54
53
|
'security middleware sets headers': async ({pass, fail}) => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
const res = createMockRes();
|
|
55
|
+
const mw = securityMiddleware({headers: {'X-Test': '1'}});
|
|
56
|
+
await mw(createMockReq(), res, async () => {});
|
|
57
|
+
|
|
58
|
+
if(res.getHeader('X-Test') !== '1') return fail('header set');
|
|
59
|
+
|
|
60
|
+
pass('security');
|
|
62
61
|
},
|
|
63
62
|
'logging middleware logs after response end': async ({pass, fail}) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
const logs = [];
|
|
64
|
+
const logger = (m) => logs.push(String(m));
|
|
65
|
+
const mw = loggingMiddleware({includeUserAgent: true, includeResponseTime: true}, logger);
|
|
66
|
+
const res = createMockRes();
|
|
67
|
+
await mw(createMockReq({headers: {'user-agent': 'UA'}}), res, async () => { res.end('x'); });
|
|
68
|
+
|
|
69
|
+
if(!(logs.length === 1 && logs[0].includes('GET /') && logs[0].includes('UA'))) return fail('logged');
|
|
70
|
+
|
|
71
|
+
pass('logging');
|
|
73
72
|
}
|
|
74
73
|
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import defaultConfig from '../src/defaultConfig.js';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { writeFile, mkdir, rm } from 'fs/promises';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Test Utilities
|
|
7
|
+
*/
|
|
8
|
+
const createTempConfig = async (config) => {
|
|
9
|
+
const tempDir = path.join(process.cwd(), 'tests', 'temp-config-test');
|
|
10
|
+
await mkdir(tempDir, { recursive: true });
|
|
11
|
+
|
|
12
|
+
const configPath = path.join(tempDir, 'test.config.json');
|
|
13
|
+
await writeFile(configPath, JSON.stringify(config, null, 2));
|
|
14
|
+
|
|
15
|
+
return { tempDir, configPath };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const cleanupTempConfig = async (tempDir) => {
|
|
19
|
+
try {
|
|
20
|
+
await rm(tempDir, { recursive: true });
|
|
21
|
+
} catch(e) {
|
|
22
|
+
// Directory might not exist
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
Lifecycle Callbacks
|
|
28
|
+
*/
|
|
29
|
+
export const afterAll = async () => {
|
|
30
|
+
const tempDir = path.join(process.cwd(), 'tests', 'temp-config-test');
|
|
31
|
+
await cleanupTempConfig(tempDir);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/*
|
|
35
|
+
Cache Configuration Tests
|
|
36
|
+
*/
|
|
37
|
+
export default {
|
|
38
|
+
'default cache configuration includes all required fields': async ({pass, fail}) => {
|
|
39
|
+
const cache = defaultConfig.cache;
|
|
40
|
+
|
|
41
|
+
if(!cache) return fail('cache config missing from defaults');
|
|
42
|
+
if(typeof cache.enabled !== 'boolean') return fail('enabled should be boolean');
|
|
43
|
+
if(typeof cache.maxSize !== 'number') return fail('maxSize should be number');
|
|
44
|
+
if(typeof cache.maxMemoryMB !== 'number') return fail('maxMemoryMB should be number');
|
|
45
|
+
if(typeof cache.ttlMs !== 'number') return fail('ttlMs should be number');
|
|
46
|
+
if(typeof cache.maxHeapUsagePercent !== 'number') return fail('maxHeapUsagePercent should be number');
|
|
47
|
+
if(typeof cache.memoryCheckInterval !== 'number') return fail('memoryCheckInterval should be number');
|
|
48
|
+
if(typeof cache.watchFiles !== 'boolean') return fail('watchFiles should be boolean');
|
|
49
|
+
if(typeof cache.enableMemoryMonitoring !== 'boolean') return fail('enableMemoryMonitoring should be boolean');
|
|
50
|
+
|
|
51
|
+
pass('Default cache configuration verified');
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
'cache can be completely disabled': async ({pass, fail}) => {
|
|
55
|
+
const { tempDir, configPath } = await createTempConfig({
|
|
56
|
+
cache: { enabled: false }
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Import router to test config loading
|
|
60
|
+
const router = await import('../src/router.js');
|
|
61
|
+
const flags = { root: 'docs', config: configPath };
|
|
62
|
+
|
|
63
|
+
const handler = await router.default(flags, () => {}); // Empty log function
|
|
64
|
+
|
|
65
|
+
if(handler.moduleCache !== null) {
|
|
66
|
+
await cleanupTempConfig(tempDir);
|
|
67
|
+
return fail('cache should be null when disabled');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await cleanupTempConfig(tempDir);
|
|
71
|
+
pass('Cache disable configuration verified');
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
'custom cache limits are applied correctly': async ({pass, fail}) => {
|
|
75
|
+
const { tempDir, configPath } = await createTempConfig({
|
|
76
|
+
cache: {
|
|
77
|
+
enabled: true,
|
|
78
|
+
maxSize: 25,
|
|
79
|
+
maxMemoryMB: 10,
|
|
80
|
+
ttlMs: 120000
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const router = await import('../src/router.js');
|
|
85
|
+
const flags = { root: 'docs', config: configPath };
|
|
86
|
+
|
|
87
|
+
const handler = await router.default(flags, () => {});
|
|
88
|
+
|
|
89
|
+
if(!handler.moduleCache) {
|
|
90
|
+
await cleanupTempConfig(tempDir);
|
|
91
|
+
return fail('cache should be enabled');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const stats = handler.getStats();
|
|
95
|
+
if(stats.cache.maxSize !== 25) {
|
|
96
|
+
handler.moduleCache.destroy();
|
|
97
|
+
await cleanupTempConfig(tempDir);
|
|
98
|
+
return fail('maxSize not applied');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if(stats.cache.maxMemoryMB !== 10) {
|
|
102
|
+
handler.moduleCache.destroy();
|
|
103
|
+
await cleanupTempConfig(tempDir);
|
|
104
|
+
return fail('maxMemoryMB not applied');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if(stats.config.ttlMs !== 120000) {
|
|
108
|
+
handler.moduleCache.destroy();
|
|
109
|
+
await cleanupTempConfig(tempDir);
|
|
110
|
+
return fail('ttlMs not applied');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
handler.moduleCache.destroy();
|
|
114
|
+
await cleanupTempConfig(tempDir);
|
|
115
|
+
pass('Custom cache configuration verified');
|
|
116
|
+
}
|
|
117
|
+
};
|
|
@@ -2,12 +2,11 @@ import defaultConfig from '../src/defaultConfig.js';
|
|
|
2
2
|
|
|
3
3
|
export default {
|
|
4
4
|
'defaultConfig contains required fields and types': async ({pass, fail}) => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} catch(e){ fail(e.message); }
|
|
5
|
+
if(typeof defaultConfig !== 'object') return fail('not object');
|
|
6
|
+
if(!defaultConfig.allowedMimes || !defaultConfig.disallowedRegex) return fail('missing keys');
|
|
7
|
+
if(!defaultConfig.routeFiles.includes('GET.js')) return fail('routeFiles missing GET.js');
|
|
8
|
+
if(!defaultConfig.middleware || typeof defaultConfig.middleware !== 'object') return fail('middleware missing');
|
|
9
|
+
|
|
10
|
+
pass('shape ok');
|
|
12
11
|
}
|
|
13
12
|
};
|
|
@@ -8,24 +8,23 @@ const {default: authMiddleware} = await import(url.pathToFileURL(examplePath));
|
|
|
8
8
|
|
|
9
9
|
export default {
|
|
10
10
|
'blocks when API key missing and allows when present': async ({pass, fail}) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if(res1.statusCode !== 401) return fail('should 401 without key');
|
|
11
|
+
await setEnv({API_KEY: 'abc'}, async () => {
|
|
12
|
+
const res1 = createMockRes();
|
|
13
|
+
await authMiddleware(createMockReq({url:'/private'}), res1, async ()=>{});
|
|
14
|
+
if(res1.statusCode !== 401) return fail('should 401 without key');
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
const res2 = createMockRes();
|
|
17
|
+
const req2 = createMockReq({headers: {'x-api-key': 'abc'}, url:'/private'});
|
|
18
|
+
let called = false;
|
|
19
|
+
await authMiddleware(req2, res2, async ()=>{ called = true; });
|
|
20
|
+
if(!called) return fail('should call next');
|
|
21
|
+
if(!(req2.user && req2.user.authenticated)) return fail('user attached');
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
const res3 = createMockRes();
|
|
24
|
+
await authMiddleware(createMockReq({url:'/public/file'}), res3, async ()=>{});
|
|
25
|
+
if(res3.isEnded() === true) return fail('public should not end');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
pass('auth middleware');
|
|
30
29
|
}
|
|
31
30
|
};
|
|
@@ -4,43 +4,43 @@ import path from 'path';
|
|
|
4
4
|
const toAbs = (root, p) => path.join(root, p);
|
|
5
5
|
|
|
6
6
|
export default {
|
|
7
|
-
'exact match returns file': async ({pass, fail
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
'exact match returns file': async ({pass, fail}) => {
|
|
8
|
+
const root = path.join(process.cwd(), 'tmp-root');
|
|
9
|
+
const files = [toAbs(root, 'a/b/GET.js')];
|
|
10
|
+
const [file, params] = await findFile(files, root, '/a/b/GET.js', 'GET', () => {});
|
|
11
|
+
|
|
12
|
+
if(file !== files[0]) return fail('not exact');
|
|
13
|
+
if(Object.keys(params).length !== 0) return fail('params present');
|
|
14
|
+
|
|
15
|
+
pass('exact');
|
|
16
16
|
},
|
|
17
|
-
'directory index prioritization and method specific': async ({pass, fail
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
'directory index prioritization and method specific': async ({pass, fail}) => {
|
|
18
|
+
const root = path.join(process.cwd(), 'tmp-root');
|
|
19
|
+
const files = ['a/index.html', 'a/GET.js', 'a/index.js'].map(p => toAbs(root, p));
|
|
20
|
+
const [file] = await findFile(files, root, '/a', 'GET', () => {});
|
|
21
|
+
|
|
22
|
+
if(!file || path.basename(file) !== 'GET.js') return fail('priority not respected');
|
|
23
|
+
|
|
24
|
+
pass('dir index');
|
|
25
25
|
},
|
|
26
|
-
'dynamic match with params and best priority': async ({pass, fail
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
'dynamic match with params and best priority': async ({pass, fail}) => {
|
|
27
|
+
const root = path.join(process.cwd(), 'tmp-root');
|
|
28
|
+
const files = ['user/[id]/GET.js', 'user/[id]/index.html', 'user/[id]/index.js'].map(p => toAbs(root, p));
|
|
29
|
+
const [file, params] = await findFile(files, root, '/user/42', 'GET', () => {});
|
|
30
|
+
|
|
31
|
+
if(!file || path.basename(file) !== 'GET.js') return fail('did not pick GET.js');
|
|
32
|
+
if(params.id !== '42') return fail('param missing');
|
|
33
|
+
|
|
34
|
+
pass('dynamic');
|
|
35
35
|
},
|
|
36
|
-
'no match returns false and empty params': async ({pass, fail
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
'no match returns false and empty params': async ({pass, fail}) => {
|
|
37
|
+
const root = path.join(process.cwd(), 'tmp-root');
|
|
38
|
+
const files = ['x/y/index.html'].map(p => toAbs(root, p));
|
|
39
|
+
const [file, params] = await findFile(files, root, '/nope', 'GET', () => {});
|
|
40
|
+
|
|
41
|
+
if(file !== false) return fail('should be false');
|
|
42
|
+
if(Object.keys(params).length !== 0) return fail('params not empty');
|
|
43
|
+
|
|
44
|
+
pass('no match');
|
|
45
45
|
}
|
|
46
46
|
};
|
|
@@ -5,21 +5,21 @@ import {withTempDir, write, log} from './test-utils.js';
|
|
|
5
5
|
|
|
6
6
|
export default {
|
|
7
7
|
'scans directories recursively and filters by mime and disallowed': async ({pass, fail}) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
8
|
+
await withTempDir(async (dir) => {
|
|
9
|
+
const cfg = JSON.parse(JSON.stringify(defaultConfig));
|
|
10
|
+
await write(dir, 'index.html', '<!doctype html>');
|
|
11
|
+
await write(dir, '.env', 'SECRET=1');
|
|
12
|
+
await write(dir, 'notes.xyz', 'unknown');
|
|
13
|
+
await write(dir, 'sub/app.js', 'console.log(1)');
|
|
14
|
+
const files = await getFiles(dir, cfg, log);
|
|
15
|
+
const rel = files.map(f => path.relative(dir, f).replace(/\\/g, '/'));
|
|
16
|
+
|
|
17
|
+
if(!rel.includes('index.html')) return fail('includes html');
|
|
18
|
+
if(!rel.includes('sub/app.js')) return fail('includes js');
|
|
19
|
+
if(rel.includes('.env')) return fail('excludes disallowed');
|
|
20
|
+
if(rel.includes('notes.xyz')) return fail('excludes unknown ext');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
pass('scan and filter');
|
|
24
24
|
}
|
|
25
25
|
};
|
|
@@ -4,27 +4,27 @@ export default {
|
|
|
4
4
|
'parses long flags with values and booleans': async ({pass, fail}) => {
|
|
5
5
|
const args = ['--port', '8080', '--scan'];
|
|
6
6
|
const flags = getFlags(args, {port: 3000, scan: false});
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
|
|
8
|
+
if(flags.port !== '8080') return fail('port not parsed');
|
|
9
|
+
if(flags.scan !== true) return fail('scan boolean not parsed');
|
|
10
|
+
|
|
11
|
+
pass('parsed long flags');
|
|
12
12
|
},
|
|
13
13
|
'parses short flags using map and preserves defaults': async ({pass, fail}) => {
|
|
14
14
|
const args = ['-p', '9090', '-s'];
|
|
15
15
|
const flags = getFlags(args, {port: 3000, scan: false}, {p: 'port', s: 'scan'});
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
|
|
17
|
+
if(flags.port !== '9090') return fail('short mapped value failed');
|
|
18
|
+
if(flags.scan !== true) return fail('short mapped boolean failed');
|
|
19
|
+
|
|
20
|
+
pass('short flags parsed');
|
|
21
21
|
},
|
|
22
22
|
'treats next arg starting with dash as boolean flag': async ({pass, fail}) => {
|
|
23
23
|
const args = ['-l', '-5', 'file'];
|
|
24
24
|
const flags = getFlags(args, {l: 2});
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
|
|
26
|
+
if(flags.l !== true) return fail('should be boolean true');
|
|
27
|
+
|
|
28
|
+
pass('dash after flag -> boolean');
|
|
29
29
|
}
|
|
30
30
|
};
|
package/tests/index.node-test.js
CHANGED
|
@@ -3,21 +3,29 @@ import path from 'path';
|
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
5
|
'index.js CLI starts server and serves root': async ({pass, fail}) => {
|
|
6
|
-
|
|
7
|
-
await
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
6
|
+
await withTempDir(async (dir) => {
|
|
7
|
+
await write(dir, 'index.html', 'home');
|
|
8
|
+
const port = randomPort();
|
|
9
|
+
const args = [path.join(process.cwd(), 'dist/index.js'), '-r', '.', '-p', String(port), '-l', '0'];
|
|
10
|
+
const child = await startNode(args, {cwd: dir});
|
|
11
|
+
// wait briefly for server to start
|
|
12
|
+
await new Promise(r => setTimeout(r, 400));
|
|
13
|
+
const {res, body} = await httpGet(`http://localhost:${port}/index.html`);
|
|
14
|
+
|
|
15
|
+
if(res.statusCode !== 200) {
|
|
16
|
+
child.kill();
|
|
17
|
+
return fail('server running');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if(body.toString() !== 'home') {
|
|
21
|
+
child.kill();
|
|
22
|
+
return fail('served');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
child.kill();
|
|
26
|
+
await new Promise(r => setTimeout(r, 50));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
pass('cli');
|
|
22
30
|
}
|
|
23
31
|
};
|
|
@@ -3,16 +3,16 @@ import {createMockReq, createMockRes} from './test-utils.js';
|
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
5
|
'runs middleware in order and calls finalHandler': async ({pass, fail}) => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
const mr = new MiddlewareRunner();
|
|
7
|
+
const calls = [];
|
|
8
|
+
mr.use(async (_req, _res, next) => { calls.push('a'); await next(); calls.push('a:after'); });
|
|
9
|
+
mr.use(async (_req, _res, next) => { calls.push('b'); await next(); calls.push('b:after'); });
|
|
10
|
+
const req = createMockReq();
|
|
11
|
+
const res = createMockRes();
|
|
12
|
+
await mr.run(req, res, async () => { calls.push('final'); });
|
|
13
|
+
|
|
14
|
+
if(calls.join(',') !== 'a,b,final,b:after,a:after') return fail('order incorrect');
|
|
15
|
+
|
|
16
|
+
pass('middleware order');
|
|
17
17
|
}
|
|
18
18
|
};
|