kempo-server 1.6.1 → 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/package.json +1 -1
- package/tests/builtinMiddleware-cors.node-test.js +10 -10
- package/tests/builtinMiddleware.node-test.js +57 -58
- package/tests/cacheConfig.node-test.js +72 -62
- 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 +180 -175
- 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 -78
package/package.json
CHANGED
|
@@ -3,15 +3,15 @@ import {createMockReq, createMockRes} from './test-utils.js';
|
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
5
|
'cors origin array and non-OPTIONS continues to next': async ({pass, fail}) => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
const res = createMockRes();
|
|
7
|
+
const mw = corsMiddleware({origin: ['http://a','http://b'], methods: ['GET'], headers: ['X']});
|
|
8
|
+
const req = createMockReq({method: 'GET', headers: {origin: 'http://b'}});
|
|
9
|
+
let called = false;
|
|
10
|
+
await mw(req, res, async () => { called = true; });
|
|
11
|
+
|
|
12
|
+
if(!called) return fail('next not called');
|
|
13
|
+
if(res.getHeader('Access-Control-Allow-Origin') !== 'http://b') return fail('allowed origin');
|
|
14
|
+
|
|
15
|
+
pass('cors array');
|
|
16
16
|
}
|
|
17
17
|
};
|
|
@@ -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
|
};
|
|
@@ -26,8 +26,7 @@ const cleanupTempConfig = async (tempDir) => {
|
|
|
26
26
|
/*
|
|
27
27
|
Lifecycle Callbacks
|
|
28
28
|
*/
|
|
29
|
-
export const afterAll = async (
|
|
30
|
-
log('Cleaning up cache configuration test environment...');
|
|
29
|
+
export const afterAll = async () => {
|
|
31
30
|
const tempDir = path.join(process.cwd(), 'tests', 'temp-config-test');
|
|
32
31
|
await cleanupTempConfig(tempDir);
|
|
33
32
|
};
|
|
@@ -36,72 +35,83 @@ export const afterAll = async (log) => {
|
|
|
36
35
|
Cache Configuration Tests
|
|
37
36
|
*/
|
|
38
37
|
export default {
|
|
39
|
-
'default cache configuration includes all required fields': async ({pass, fail
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
log('✓ Default cache configuration structure valid');
|
|
54
|
-
pass('Default cache configuration verified');
|
|
55
|
-
} catch(e){ fail(e.message); }
|
|
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');
|
|
56
52
|
},
|
|
57
53
|
|
|
58
|
-
'cache can be completely disabled': async ({pass, fail
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if(handler.moduleCache !== null) throw new Error('cache should be null when disabled');
|
|
71
|
-
|
|
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) {
|
|
72
66
|
await cleanupTempConfig(tempDir);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
67
|
+
return fail('cache should be null when disabled');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await cleanupTempConfig(tempDir);
|
|
71
|
+
pass('Cache disable configuration verified');
|
|
76
72
|
},
|
|
77
73
|
|
|
78
|
-
'custom cache limits are applied correctly': async ({pass, fail
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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) {
|
|
101
108
|
handler.moduleCache.destroy();
|
|
102
109
|
await cleanupTempConfig(tempDir);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
return fail('ttlMs not applied');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
handler.moduleCache.destroy();
|
|
114
|
+
await cleanupTempConfig(tempDir);
|
|
115
|
+
pass('Custom cache configuration verified');
|
|
106
116
|
}
|
|
107
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
|
};
|