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
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import ModuleCache from '../src/moduleCache.js';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { writeFile, unlink, stat, mkdir, rm } from 'fs/promises';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Test Utilities
|
|
7
|
+
*/
|
|
8
|
+
const activeCaches = [];
|
|
9
|
+
|
|
10
|
+
const createTestCache = (config = {}) => {
|
|
11
|
+
const cache = new ModuleCache({
|
|
12
|
+
enableMemoryMonitoring: false, // Always disable to prevent hanging
|
|
13
|
+
watchFiles: false, // Default to false for most tests
|
|
14
|
+
...config
|
|
15
|
+
});
|
|
16
|
+
activeCaches.push(cache);
|
|
17
|
+
return cache;
|
|
18
|
+
};
|
|
19
|
+
const createTempDir = async () => {
|
|
20
|
+
const tempDir = path.join(process.cwd(), 'tests', 'temp-cache-test');
|
|
21
|
+
try {
|
|
22
|
+
await mkdir(tempDir, { recursive: true });
|
|
23
|
+
} catch(e) {
|
|
24
|
+
// Directory might already exist
|
|
25
|
+
}
|
|
26
|
+
return tempDir;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const cleanupTempDir = async (tempDir) => {
|
|
30
|
+
try {
|
|
31
|
+
await rm(tempDir, { recursive: true });
|
|
32
|
+
} catch(e) {
|
|
33
|
+
// Directory might not exist
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/*
|
|
38
|
+
Lifecycle Callbacks
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
export const afterAll = async () => {
|
|
42
|
+
// Destroy all active caches to clean up timers and watchers
|
|
43
|
+
for(let i = 0; i < activeCaches.length; i++) {
|
|
44
|
+
activeCaches[i].destroy();
|
|
45
|
+
}
|
|
46
|
+
activeCaches.length = 0;
|
|
47
|
+
|
|
48
|
+
// Give file watchers extra time to fully close
|
|
49
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
50
|
+
|
|
51
|
+
const tempDir = path.join(process.cwd(), 'tests', 'temp-cache-test');
|
|
52
|
+
await cleanupTempDir(tempDir);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/*
|
|
56
|
+
Module Cache Tests
|
|
57
|
+
*/
|
|
58
|
+
export default {
|
|
59
|
+
'basic LRU functionality works correctly': async ({pass, fail, log}) => {
|
|
60
|
+
const cache = createTestCache({
|
|
61
|
+
maxSize: 2,
|
|
62
|
+
maxMemoryMB: 10,
|
|
63
|
+
ttlMs: 60000,
|
|
64
|
+
watchFiles: false
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const mockStats1 = { mtime: new Date('2023-01-01') };
|
|
68
|
+
const mockStats2 = { mtime: new Date('2023-01-02') };
|
|
69
|
+
const mockStats3 = { mtime: new Date('2023-01-03') };
|
|
70
|
+
|
|
71
|
+
cache.set('/test1.js', { default: () => 'test1' }, mockStats1, 1);
|
|
72
|
+
if(cache.cache.size !== 1) {
|
|
73
|
+
cache.destroy();
|
|
74
|
+
return fail('size should be 1');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
cache.set('/test2.js', { default: () => 'test2' }, mockStats2, 1);
|
|
78
|
+
if(cache.cache.size !== 2) {
|
|
79
|
+
cache.destroy();
|
|
80
|
+
return fail('size should be 2');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
cache.set('/test3.js', { default: () => 'test3' }, mockStats3, 1);
|
|
84
|
+
if(cache.cache.size !== 2) {
|
|
85
|
+
cache.destroy();
|
|
86
|
+
return fail('size should still be 2');
|
|
87
|
+
}
|
|
88
|
+
if(cache.get('/test1.js', mockStats1) !== null) {
|
|
89
|
+
cache.destroy();
|
|
90
|
+
return fail('oldest should be evicted');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
cache.destroy();
|
|
94
|
+
pass('LRU functionality verified');
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
'TTL expiration invalidates entries': async ({pass, fail, log}) => {
|
|
98
|
+
const cache = createTestCache({
|
|
99
|
+
maxSize: 10,
|
|
100
|
+
ttlMs: 50,
|
|
101
|
+
watchFiles: false
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const mockStats = { mtime: new Date('2023-01-01') };
|
|
105
|
+
cache.set('/test.js', { default: () => 'test' }, mockStats, 1);
|
|
106
|
+
|
|
107
|
+
if(cache.get('/test.js', mockStats) === null) {
|
|
108
|
+
cache.destroy();
|
|
109
|
+
return fail('should be available immediately');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
113
|
+
|
|
114
|
+
if(cache.get('/test.js', mockStats) !== null) {
|
|
115
|
+
cache.destroy();
|
|
116
|
+
return fail('should be expired');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
cache.destroy();
|
|
120
|
+
pass('TTL expiration verified');
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
'file modification invalidates cache entry': async ({pass, fail, log}) => {
|
|
124
|
+
const cache = createTestCache({
|
|
125
|
+
maxSize: 10,
|
|
126
|
+
ttlMs: 60000,
|
|
127
|
+
watchFiles: false
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const oldStats = { mtime: new Date('2023-01-01') };
|
|
131
|
+
const newStats = { mtime: new Date('2023-01-02') };
|
|
132
|
+
|
|
133
|
+
cache.set('/test.js', { default: () => 'test' }, oldStats, 1);
|
|
134
|
+
|
|
135
|
+
if(cache.get('/test.js', oldStats) === null) {
|
|
136
|
+
cache.destroy();
|
|
137
|
+
return fail('should be available with old stats');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if(cache.get('/test.js', newStats) !== null) {
|
|
141
|
+
cache.destroy();
|
|
142
|
+
return fail('should be invalidated with newer stats');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
cache.destroy();
|
|
146
|
+
pass('File modification invalidation verified');
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
'memory limit enforcement evicts oldest entries': async ({pass, fail, log}) => {
|
|
150
|
+
const cache = createTestCache({
|
|
151
|
+
maxSize: 100,
|
|
152
|
+
maxMemoryMB: 0.002,
|
|
153
|
+
ttlMs: 60000,
|
|
154
|
+
watchFiles: false
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const mockStats = { mtime: new Date('2023-01-01') };
|
|
158
|
+
|
|
159
|
+
cache.set('/test1.js', { default: () => 'test1' }, mockStats, 1);
|
|
160
|
+
cache.set('/test2.js', { default: () => 'test2' }, mockStats, 1);
|
|
161
|
+
cache.set('/test3.js', { default: () => 'test3' }, mockStats, 1);
|
|
162
|
+
|
|
163
|
+
if(cache.get('/test1.js', mockStats) !== null) {
|
|
164
|
+
cache.destroy();
|
|
165
|
+
return fail('first entry should be evicted due to memory limit');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if(cache.get('/test2.js', mockStats) === null) {
|
|
169
|
+
cache.destroy();
|
|
170
|
+
return fail('second entry should still be cached');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if(cache.get('/test3.js', mockStats) === null) {
|
|
174
|
+
cache.destroy();
|
|
175
|
+
return fail('third entry should still be cached');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
cache.destroy();
|
|
179
|
+
pass('Memory limit enforcement verified');
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
'statistics tracking works correctly': async ({pass, fail, log}) => {
|
|
183
|
+
const cache = createTestCache({
|
|
184
|
+
maxSize: 10,
|
|
185
|
+
watchFiles: false
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const mockStats = { mtime: new Date('2023-01-01') };
|
|
189
|
+
|
|
190
|
+
if(cache.get('/test.js', mockStats) !== null) {
|
|
191
|
+
cache.destroy();
|
|
192
|
+
return fail('should be cache miss');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if(cache.stats.misses !== 1) {
|
|
196
|
+
cache.destroy();
|
|
197
|
+
return fail('miss count should be 1');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
cache.set('/test.js', { default: () => 'test' }, mockStats, 1);
|
|
201
|
+
|
|
202
|
+
if(cache.get('/test.js', mockStats) === null) {
|
|
203
|
+
cache.destroy();
|
|
204
|
+
return fail('should be cache hit');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if(cache.stats.hits !== 1) {
|
|
208
|
+
cache.destroy();
|
|
209
|
+
return fail('hit count should be 1');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if(cache.getHitRate() !== 50) {
|
|
213
|
+
cache.destroy();
|
|
214
|
+
return fail('hit rate should be 50%');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const stats = cache.getStats();
|
|
218
|
+
if(!stats.cache || !stats.stats || !stats.memory) {
|
|
219
|
+
cache.destroy();
|
|
220
|
+
return fail('stats structure invalid');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
cache.destroy();
|
|
224
|
+
pass('Statistics tracking verified');
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
'file watching invalidates cache on changes': async ({pass, fail, log}) => {
|
|
228
|
+
let cache;
|
|
229
|
+
let testFilePath;
|
|
230
|
+
|
|
231
|
+
const tempDir = await createTempDir();
|
|
232
|
+
|
|
233
|
+
cache = createTestCache({
|
|
234
|
+
maxSize: 10,
|
|
235
|
+
ttlMs: 60000,
|
|
236
|
+
watchFiles: true
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
testFilePath = path.join(tempDir, 'watch-test.js');
|
|
240
|
+
|
|
241
|
+
await writeFile(testFilePath, 'export default () => "v1";');
|
|
242
|
+
const initialStats = await stat(testFilePath);
|
|
243
|
+
|
|
244
|
+
const module = { default: () => 'v1' };
|
|
245
|
+
cache.set(testFilePath, module, initialStats, 1);
|
|
246
|
+
|
|
247
|
+
if(cache.get(testFilePath, initialStats) === null) {
|
|
248
|
+
await unlink(testFilePath);
|
|
249
|
+
cache.destroy();
|
|
250
|
+
return fail('should be in cache initially');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
254
|
+
await writeFile(testFilePath, 'export default () => "v2";');
|
|
255
|
+
|
|
256
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
257
|
+
|
|
258
|
+
if(cache.get(testFilePath, initialStats) !== null) {
|
|
259
|
+
await unlink(testFilePath);
|
|
260
|
+
cache.destroy();
|
|
261
|
+
return fail('should be invalidated after file change');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if(cache.stats.fileChanges === 0) {
|
|
265
|
+
await unlink(testFilePath);
|
|
266
|
+
cache.destroy();
|
|
267
|
+
return fail('file change should be tracked');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Clean up immediately
|
|
271
|
+
await unlink(testFilePath);
|
|
272
|
+
cache.destroy();
|
|
273
|
+
|
|
274
|
+
// Give extra time for file watcher cleanup
|
|
275
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
276
|
+
|
|
277
|
+
pass('File watching verified');
|
|
278
|
+
}
|
|
279
|
+
};
|
|
@@ -3,32 +3,31 @@ import createRequestWrapper from '../src/requestWrapper.js';
|
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
5
|
'parses query and path and provides params': async ({pass, fail}) => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
const req = createMockReq({url: '/user/123?x=1&y=2', headers: {host: 'localhost'}});
|
|
7
|
+
const wrapped = createRequestWrapper(req, {id: '123'});
|
|
8
|
+
|
|
9
|
+
if(wrapped.path !== '/user/123') return fail('path');
|
|
10
|
+
if(!(wrapped.query.x === '1' && wrapped.query.y === '2')) return fail('query');
|
|
11
|
+
if(wrapped.params.id !== '123') return fail('params');
|
|
12
|
+
|
|
13
|
+
pass('parsed url');
|
|
14
14
|
},
|
|
15
15
|
'body/json/text/buffer helpers work': async ({pass, fail}) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if(text !== JSON.stringify(payload)) return fail('text');
|
|
16
|
+
const payload = {a: 1};
|
|
17
|
+
// Each body reader must have its own stream instance
|
|
18
|
+
const reqText = createMockReq({method: 'POST', url: '/', headers: {host: 'x', 'content-type': 'application/json'}, body: JSON.stringify(payload)});
|
|
19
|
+
const text = await createRequestWrapper(reqText).text();
|
|
20
|
+
if(text !== JSON.stringify(payload)) return fail('text');
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
const reqJson = createMockReq({method: 'POST', url: '/', headers: {host: 'x', 'content-type': 'application/json'}, body: JSON.stringify(payload)});
|
|
23
|
+
const obj = await createRequestWrapper(reqJson).json();
|
|
24
|
+
if(obj.a !== 1) return fail('json');
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
const reqBuf = createMockReq({url: '/', headers: {host: 'x'}, body: 'abc'});
|
|
27
|
+
const buf = await createRequestWrapper(reqBuf).buffer();
|
|
28
|
+
if(!(Buffer.isBuffer(buf) && buf.toString() === 'abc')) return fail('buffer');
|
|
29
|
+
|
|
30
|
+
pass('helpers');
|
|
32
31
|
},
|
|
33
32
|
'invalid json throws': async ({pass, fail}) => {
|
|
34
33
|
const req = createMockReq({url: '/', headers: {host: 'x'}, body: 'not json'});
|
|
@@ -40,12 +39,12 @@ export default {
|
|
|
40
39
|
}
|
|
41
40
|
},
|
|
42
41
|
'get and is helpers': async ({pass, fail}) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
const req = createMockReq({url: '/', headers: {'content-type': 'text/plain', host: 'x'}});
|
|
43
|
+
const w = createRequestWrapper(req);
|
|
44
|
+
|
|
45
|
+
if(w.get('content-type') !== 'text/plain') return fail('get');
|
|
46
|
+
if(w.is('text/plain') !== true) return fail('is');
|
|
47
|
+
|
|
48
|
+
pass('header helpers');
|
|
50
49
|
}
|
|
51
50
|
};
|
|
@@ -3,72 +3,72 @@ import createResponseWrapper from '../src/responseWrapper.js';
|
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
5
|
'status and set/get headers and type': async ({pass, fail}) => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
const res = createMockRes();
|
|
7
|
+
const w = createResponseWrapper(res);
|
|
8
|
+
w.status(201).set('X-Test', '1').type('json');
|
|
9
|
+
|
|
10
|
+
if(res.statusCode !== 201) return fail('status');
|
|
11
|
+
if(res.getHeader('X-Test') !== '1') return fail('set/get');
|
|
12
|
+
if(res.getHeader('Content-Type') !== 'application/json') return fail('type');
|
|
13
|
+
|
|
14
|
+
pass('status+headers+type');
|
|
15
15
|
},
|
|
16
16
|
'json sends and prevents further changes': async ({pass, fail}) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
const res = createMockRes();
|
|
18
|
+
const w = createResponseWrapper(res);
|
|
19
|
+
w.json({a: 1});
|
|
20
|
+
|
|
21
|
+
if(!res.isEnded()) return fail('ended');
|
|
22
|
+
|
|
23
|
+
try { w.set('X', 'y'); fail('should not set after send'); } catch(_){ /* ok */ }
|
|
24
|
+
|
|
25
|
+
pass('json');
|
|
25
26
|
},
|
|
26
27
|
'send handles string, object, buffer and null': async ({pass, fail}) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if(res1.getBody().toString() !== 'hello') return fail('string body');
|
|
28
|
+
const res1 = createMockRes();
|
|
29
|
+
createResponseWrapper(res1).send('hello');
|
|
30
|
+
// Content-Type defaults to text/html for string when not set
|
|
31
|
+
if(res1.getHeader('Content-Type') !== 'text/html') return fail('string content-type');
|
|
32
|
+
if(res1.getBody().toString() !== 'hello') return fail('string body');
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
const res2 = createMockRes();
|
|
35
|
+
createResponseWrapper(res2).send({a:1});
|
|
36
|
+
if(res2.getHeader('Content-Type') !== 'application/json') return fail('object content-type');
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
const res3 = createMockRes();
|
|
39
|
+
const buf = Buffer.from('abc');
|
|
40
|
+
createResponseWrapper(res3).send(buf);
|
|
41
|
+
const body3 = res3.getBody().toString();
|
|
42
|
+
if(!body3.includes('"data"')) return fail('buffer equal');
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
const res4 = createMockRes();
|
|
45
|
+
createResponseWrapper(res4).send(null);
|
|
46
|
+
if(!res4.isEnded()) return fail('null ended');
|
|
47
|
+
|
|
48
|
+
pass('send variants');
|
|
49
49
|
},
|
|
50
50
|
'html and text helpers': async ({pass, fail}) => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if(r1.getHeader('Content-Type') !== 'text/html') return fail('html type');
|
|
51
|
+
const r1 = createMockRes();
|
|
52
|
+
createResponseWrapper(r1).html('<h1>Ok</h1>');
|
|
53
|
+
if(r1.getHeader('Content-Type') !== 'text/html') return fail('html type');
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
const r2 = createMockRes();
|
|
56
|
+
createResponseWrapper(r2).text('plain');
|
|
57
|
+
if(r2.getHeader('Content-Type') !== 'text/plain') return fail('text type');
|
|
58
|
+
|
|
59
|
+
pass('helpers');
|
|
61
60
|
},
|
|
62
61
|
'redirect and cookies': async ({pass, fail}) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
62
|
+
const r = createMockRes();
|
|
63
|
+
const w = createResponseWrapper(r);
|
|
64
|
+
w.cookie('a', 'b', {httpOnly: true, path: '/'});
|
|
65
|
+
const cookies = parseCookies(r.getHeader('Set-Cookie'));
|
|
66
|
+
|
|
67
|
+
if(!(cookies.length === 1 && cookies[0].includes('a=b'))) return fail('cookie added');
|
|
68
|
+
|
|
69
|
+
w.redirect('/next', 301);
|
|
70
|
+
if(!(r.statusCode === 301 && r.getHeader('Location') === '/next')) return fail('redirect');
|
|
71
|
+
|
|
72
|
+
pass('redirect+cookie');
|
|
73
73
|
}
|
|
74
74
|
};
|
|
@@ -4,43 +4,55 @@ import router from '../src/router.js';
|
|
|
4
4
|
|
|
5
5
|
export default {
|
|
6
6
|
'built-in middleware can be configured on router': async ({pass, fail}) => {
|
|
7
|
-
|
|
8
|
-
await
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
if(one.r.statusCode !== 200) return fail('first ok');
|
|
35
|
-
const two = await new Promise((res)=>{
|
|
36
|
-
get(`http://localhost:${port}/index.html`, r =>{
|
|
37
|
-
const chunks = []; r.on('data', c => chunks.push(c)); r.on('end', ()=> res({r, body: Buffer.concat(chunks)}));
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
if(two.r.statusCode !== 429) return fail('rate limited');
|
|
41
|
-
} finally { server.close(); process.chdir(prev); }
|
|
7
|
+
await withTempDir(async (dir) => {
|
|
8
|
+
await write(dir, '.config.json', JSON.stringify({
|
|
9
|
+
middleware: {
|
|
10
|
+
cors: {enabled: true, origin: '*', methods: ['GET'], headers: ['X']},
|
|
11
|
+
compression: {enabled: true, threshold: 1},
|
|
12
|
+
rateLimit: {enabled: true, maxRequests: 1, windowMs: 1000, message: 'Too many'},
|
|
13
|
+
security: {enabled: true, headers: {'X-Test':'1'}},
|
|
14
|
+
logging: {enabled: true, includeUserAgent: false, includeResponseTime: true}
|
|
15
|
+
}
|
|
16
|
+
}));
|
|
17
|
+
await write(dir, 'index.html', 'hello world');
|
|
18
|
+
const prev = process.cwd();
|
|
19
|
+
process.chdir(dir);
|
|
20
|
+
const flags = {root: '.', logging: 0, scan: false};
|
|
21
|
+
const handler = await router(flags, () => {});
|
|
22
|
+
const server = http.createServer(handler);
|
|
23
|
+
const port = randomPort();
|
|
24
|
+
await new Promise(r => server.listen(port, r));
|
|
25
|
+
await new Promise(r => setTimeout(r, 50));
|
|
26
|
+
|
|
27
|
+
const {get} = await import('http');
|
|
28
|
+
const one = await new Promise((res)=>{
|
|
29
|
+
get(`http://localhost:${port}/index.html`, {headers: {'accept-encoding': 'gzip'}}, r =>{
|
|
30
|
+
const chunks = []; r.on('data', c => chunks.push(c)); r.on('end', ()=> res({r, body: Buffer.concat(chunks)}));
|
|
31
|
+
});
|
|
42
32
|
});
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
|
|
34
|
+
if(one.r.statusCode !== 200) {
|
|
35
|
+
server.close();
|
|
36
|
+
process.chdir(prev);
|
|
37
|
+
return fail('first ok');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const two = await new Promise((res)=>{
|
|
41
|
+
get(`http://localhost:${port}/index.html`, r =>{
|
|
42
|
+
const chunks = []; r.on('data', c => chunks.push(c)); r.on('end', ()=> res({r, body: Buffer.concat(chunks)}));
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if(two.r.statusCode !== 429) {
|
|
47
|
+
server.close();
|
|
48
|
+
process.chdir(prev);
|
|
49
|
+
return fail('rate limited');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
server.close();
|
|
53
|
+
process.chdir(prev);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
pass('router middleware');
|
|
45
57
|
}
|
|
46
58
|
};
|