kempo-server 1.6.1 → 1.6.3
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
|
@@ -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
|
};
|
|
@@ -37,31 +37,19 @@ const cleanupTempDir = async (tempDir) => {
|
|
|
37
37
|
/*
|
|
38
38
|
Lifecycle Callbacks
|
|
39
39
|
*/
|
|
40
|
-
export const beforeAll = async (log) => {
|
|
41
|
-
log('Setting up module cache test environment...');
|
|
42
|
-
};
|
|
43
40
|
|
|
44
|
-
export const afterAll = async (
|
|
45
|
-
log('Starting cleanup of module cache test environment...');
|
|
46
|
-
|
|
47
|
-
log(`Found ${activeCaches.length} active caches to destroy`);
|
|
41
|
+
export const afterAll = async () => {
|
|
48
42
|
// Destroy all active caches to clean up timers and watchers
|
|
49
43
|
for(let i = 0; i < activeCaches.length; i++) {
|
|
50
|
-
log(`Destroying cache ${i + 1}/${activeCaches.length}`);
|
|
51
44
|
activeCaches[i].destroy();
|
|
52
45
|
}
|
|
53
46
|
activeCaches.length = 0;
|
|
54
|
-
log('All caches destroyed');
|
|
55
47
|
|
|
56
48
|
// Give file watchers extra time to fully close
|
|
57
|
-
log('Waiting for file watchers to close...');
|
|
58
49
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
59
|
-
log('File watcher wait complete');
|
|
60
50
|
|
|
61
51
|
const tempDir = path.join(process.cwd(), 'tests', 'temp-cache-test');
|
|
62
|
-
log('Cleaning up temp directory...');
|
|
63
52
|
await cleanupTempDir(tempDir);
|
|
64
|
-
log('Module cache test cleanup complete');
|
|
65
53
|
};
|
|
66
54
|
|
|
67
55
|
/*
|
|
@@ -69,206 +57,223 @@ export const afterAll = async (log) => {
|
|
|
69
57
|
*/
|
|
70
58
|
export default {
|
|
71
59
|
'basic LRU functionality works correctly': async ({pass, fail, log}) => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const mockStats1 = { mtime: new Date('2023-01-01') };
|
|
81
|
-
const mockStats2 = { mtime: new Date('2023-01-02') };
|
|
82
|
-
const mockStats3 = { mtime: new Date('2023-01-03') };
|
|
60
|
+
const cache = createTestCache({
|
|
61
|
+
maxSize: 2,
|
|
62
|
+
maxMemoryMB: 10,
|
|
63
|
+
ttlMs: 60000,
|
|
64
|
+
watchFiles: false
|
|
65
|
+
});
|
|
83
66
|
|
|
84
|
-
|
|
85
|
-
|
|
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') };
|
|
86
70
|
|
|
87
|
-
|
|
88
|
-
|
|
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
|
+
}
|
|
89
76
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
}
|
|
93
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) {
|
|
94
89
|
cache.destroy();
|
|
95
|
-
|
|
96
|
-
pass('LRU functionality verified');
|
|
97
|
-
} catch(e){
|
|
98
|
-
cache?.destroy();
|
|
99
|
-
fail(e.message);
|
|
90
|
+
return fail('oldest should be evicted');
|
|
100
91
|
}
|
|
92
|
+
|
|
93
|
+
cache.destroy();
|
|
94
|
+
pass('LRU functionality verified');
|
|
101
95
|
},
|
|
102
96
|
|
|
103
97
|
'TTL expiration invalidates entries': async ({pass, fail, log}) => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
});
|
|
98
|
+
const cache = createTestCache({
|
|
99
|
+
maxSize: 10,
|
|
100
|
+
ttlMs: 50,
|
|
101
|
+
watchFiles: false
|
|
102
|
+
});
|
|
110
103
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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) {
|
|
120
115
|
cache.destroy();
|
|
121
|
-
|
|
122
|
-
pass('TTL expiration verified');
|
|
123
|
-
} catch(e){
|
|
124
|
-
cache?.destroy();
|
|
125
|
-
fail(e.message);
|
|
116
|
+
return fail('should be expired');
|
|
126
117
|
}
|
|
118
|
+
|
|
119
|
+
cache.destroy();
|
|
120
|
+
pass('TTL expiration verified');
|
|
127
121
|
},
|
|
128
122
|
|
|
129
123
|
'file modification invalidates cache entry': async ({pass, fail, log}) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
});
|
|
124
|
+
const cache = createTestCache({
|
|
125
|
+
maxSize: 10,
|
|
126
|
+
ttlMs: 60000,
|
|
127
|
+
watchFiles: false
|
|
128
|
+
});
|
|
136
129
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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) {
|
|
145
141
|
cache.destroy();
|
|
146
|
-
|
|
147
|
-
pass('File modification invalidation verified');
|
|
148
|
-
} catch(e){
|
|
149
|
-
cache?.destroy();
|
|
150
|
-
fail(e.message);
|
|
142
|
+
return fail('should be invalidated with newer stats');
|
|
151
143
|
}
|
|
144
|
+
|
|
145
|
+
cache.destroy();
|
|
146
|
+
pass('File modification invalidation verified');
|
|
152
147
|
},
|
|
153
148
|
|
|
154
149
|
'memory limit enforcement evicts oldest entries': async ({pass, fail, log}) => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
});
|
|
150
|
+
const cache = createTestCache({
|
|
151
|
+
maxSize: 100,
|
|
152
|
+
maxMemoryMB: 0.002,
|
|
153
|
+
ttlMs: 60000,
|
|
154
|
+
watchFiles: false
|
|
155
|
+
});
|
|
162
156
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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) {
|
|
173
174
|
cache.destroy();
|
|
174
|
-
|
|
175
|
-
pass('Memory limit enforcement verified');
|
|
176
|
-
} catch(e){
|
|
177
|
-
cache?.destroy();
|
|
178
|
-
fail(e.message);
|
|
175
|
+
return fail('third entry should still be cached');
|
|
179
176
|
}
|
|
177
|
+
|
|
178
|
+
cache.destroy();
|
|
179
|
+
pass('Memory limit enforcement verified');
|
|
180
180
|
},
|
|
181
181
|
|
|
182
182
|
'statistics tracking works correctly': async ({pass, fail, log}) => {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
});
|
|
183
|
+
const cache = createTestCache({
|
|
184
|
+
maxSize: 10,
|
|
185
|
+
watchFiles: false
|
|
186
|
+
});
|
|
188
187
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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) {
|
|
203
213
|
cache.destroy();
|
|
204
|
-
|
|
205
|
-
pass('Statistics tracking verified');
|
|
206
|
-
} catch(e){
|
|
207
|
-
cache?.destroy();
|
|
208
|
-
fail(e.message);
|
|
214
|
+
return fail('hit rate should be 50%');
|
|
209
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');
|
|
210
225
|
},
|
|
211
226
|
|
|
212
227
|
'file watching invalidates cache on changes': async ({pass, fail, log}) => {
|
|
213
228
|
let cache;
|
|
214
229
|
let testFilePath;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
watchFiles: true
|
|
224
|
-
});
|
|
225
|
-
log('Created cache with file watching enabled');
|
|
230
|
+
|
|
231
|
+
const tempDir = await createTempDir();
|
|
232
|
+
|
|
233
|
+
cache = createTestCache({
|
|
234
|
+
maxSize: 10,
|
|
235
|
+
ttlMs: 60000,
|
|
236
|
+
watchFiles: true
|
|
237
|
+
});
|
|
226
238
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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) {
|
|
252
265
|
await unlink(testFilePath);
|
|
253
|
-
log('Deleted test file');
|
|
254
266
|
cache.destroy();
|
|
255
|
-
|
|
256
|
-
cache = null; // Ensure it's not destroyed again in catch
|
|
257
|
-
|
|
258
|
-
// Give extra time for file watcher cleanup
|
|
259
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
260
|
-
log('File watching test cleanup complete');
|
|
261
|
-
|
|
262
|
-
log('✓ File watching invalidation working');
|
|
263
|
-
pass('File watching verified');
|
|
264
|
-
} catch(e){
|
|
265
|
-
log(`Error in file watching test: ${e.message}`);
|
|
266
|
-
if (testFilePath) {
|
|
267
|
-
try { await unlink(testFilePath); log('Cleaned up test file after error'); } catch {}
|
|
268
|
-
}
|
|
269
|
-
cache?.destroy();
|
|
270
|
-
log('Destroyed cache after error');
|
|
271
|
-
fail(e.message);
|
|
267
|
+
return fail('file change should be tracked');
|
|
272
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');
|
|
273
278
|
}
|
|
274
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
|
};
|