koa-classic-server 2.6.0 → 3.0.0-alpha.0
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 +68 -10
- package/__tests__/caching-headers.test.js +30 -30
- package/__tests__/compression-fixtures/data.json +1 -0
- package/__tests__/compression-fixtures/large.txt +1 -0
- package/__tests__/compression-fixtures/small.txt +1 -0
- package/__tests__/compression.test.js +270 -0
- package/__tests__/customTest/serversToLoad.util.js +1 -1
- package/__tests__/deprecation-warnings.test.js +71 -183
- package/__tests__/dt-unknown.test.js +635 -0
- package/__tests__/hidden-fixtures/.dot-dir/inside.txt +1 -0
- package/__tests__/hidden-fixtures/.env +2 -0
- package/__tests__/hidden-fixtures/.well-known/acme-challenge.txt +1 -0
- package/__tests__/hidden-fixtures/config/secrets/password.txt +1 -0
- package/__tests__/hidden-fixtures/data.key +1 -0
- package/__tests__/hidden-fixtures/file.secret +1 -0
- package/__tests__/hidden-fixtures/index.html +1 -0
- package/__tests__/hidden-fixtures/normal.txt +1 -0
- package/__tests__/hidden-fixtures/subdir/.env +1 -0
- package/__tests__/hidden-fixtures/subdir/regular.txt +1 -0
- package/__tests__/hidden-option.test.js +422 -0
- package/__tests__/index-option.test.js +18 -16
- package/__tests__/index.test.js +8 -4
- package/__tests__/range-fixtures/sample.txt +1 -0
- package/__tests__/range.test.js +223 -0
- package/__tests__/security-headers.test.js +153 -0
- package/__tests__/security.test.js +145 -159
- package/__tests__/server-cache-fixtures/large.txt +1 -0
- package/__tests__/server-cache-fixtures/small.txt +1 -0
- package/__tests__/server-cache.test.js +423 -0
- package/__tests__/symlink.test.js +8 -5
- package/docs/ACTION_PLAN.md +293 -0
- package/docs/CHANGELOG.md +118 -0
- package/docs/EXAMPLES_INDEX_OPTION.md +2 -2
- package/docs/FLOW_DIAGRAM.md +13 -13
- package/docs/OPTIMIZATION_ROADMAP_for_V3.md +864 -0
- package/docs/PERFORMANCE_COMPARISON.md +7 -7
- package/eslint.config.mjs +17 -0
- package/index.cjs +1114 -378
- package/index.mjs +1 -5
- package/package.json +4 -1
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* serverCache tests for koa-classic-server
|
|
3
|
+
*
|
|
4
|
+
* Covers serverCache.rawFile and serverCache.compressedFile behaviour:
|
|
5
|
+
* - rawFile disabled by default; opt-in via { serverCache: { rawFile: { enabled: true } } }
|
|
6
|
+
* - compressedFile enabled by default
|
|
7
|
+
* - LFU eviction when maxSize is exceeded
|
|
8
|
+
* - warnInterval throttling of "maxSize reached" warnings
|
|
9
|
+
* - rawFile buffer fed to template render as 4th parameter
|
|
10
|
+
* - rawFile buffer used for Range (206) responses — zero additional disk I/O
|
|
11
|
+
* - rawFile buffer used as input to compression — avoids redundant readFile
|
|
12
|
+
* - Files exceeding maxFileSize are never cached (served via stream)
|
|
13
|
+
* - Cache invalidation when mtime or size changes
|
|
14
|
+
*
|
|
15
|
+
* Fixtures (server-cache-fixtures/):
|
|
16
|
+
* small.txt — 64 bytes of 'A'
|
|
17
|
+
* large.txt — 2048 bytes of 'B' (used for maxFileSize threshold tests)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const Koa = require('koa');
|
|
24
|
+
const supertest = require('supertest');
|
|
25
|
+
const koaClassicServer = require('../index.cjs');
|
|
26
|
+
|
|
27
|
+
const fixturesDir = path.join(__dirname, 'server-cache-fixtures');
|
|
28
|
+
|
|
29
|
+
function createApp(opts = {}) {
|
|
30
|
+
const app = new Koa();
|
|
31
|
+
app.use(koaClassicServer(fixturesDir, { showDirContents: false, ...opts }));
|
|
32
|
+
return app.listen();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Default behaviour ────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
describe('serverCache.rawFile — disabled by default', () => {
|
|
38
|
+
let server;
|
|
39
|
+
beforeAll(() => { server = createApp(); });
|
|
40
|
+
afterAll(() => server.close());
|
|
41
|
+
|
|
42
|
+
test('GET /small.txt returns 200 without rawFile cache', async () => {
|
|
43
|
+
const res = await supertest(server)
|
|
44
|
+
.get('/small.txt')
|
|
45
|
+
.set('Accept-Encoding', 'identity');
|
|
46
|
+
expect(res.status).toBe(200);
|
|
47
|
+
expect(res.text).toBe('A'.repeat(64));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ─── rawFile cache enabled ────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
describe('serverCache.rawFile — enabled', () => {
|
|
54
|
+
let server;
|
|
55
|
+
beforeAll(() => {
|
|
56
|
+
server = createApp({ serverCache: { rawFile: { enabled: true } } });
|
|
57
|
+
});
|
|
58
|
+
afterAll(() => server.close());
|
|
59
|
+
|
|
60
|
+
test('first request populates cache and returns correct body', async () => {
|
|
61
|
+
const res = await supertest(server)
|
|
62
|
+
.get('/small.txt')
|
|
63
|
+
.set('Accept-Encoding', 'identity');
|
|
64
|
+
expect(res.status).toBe(200);
|
|
65
|
+
expect(res.text).toBe('A'.repeat(64));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('second request also returns correct body (served from cache)', async () => {
|
|
69
|
+
const res = await supertest(server)
|
|
70
|
+
.get('/small.txt')
|
|
71
|
+
.set('Accept-Encoding', 'identity');
|
|
72
|
+
expect(res.status).toBe(200);
|
|
73
|
+
expect(res.text).toBe('A'.repeat(64));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('Content-Length matches file size', async () => {
|
|
77
|
+
const res = await supertest(server)
|
|
78
|
+
.get('/small.txt')
|
|
79
|
+
.set('Accept-Encoding', 'identity');
|
|
80
|
+
expect(Number(res.headers['content-length'])).toBe(64);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ─── maxFileSize threshold ────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
describe('serverCache.rawFile — maxFileSize', () => {
|
|
87
|
+
test('file exceeding maxFileSize is served via stream (not cached)', async () => {
|
|
88
|
+
// maxFileSize: 32 bytes — small.txt (64 bytes) exceeds this
|
|
89
|
+
const server = createApp({
|
|
90
|
+
serverCache: { rawFile: { enabled: true, maxFileSize: 32 } }
|
|
91
|
+
});
|
|
92
|
+
const res = await supertest(server)
|
|
93
|
+
.get('/small.txt')
|
|
94
|
+
.set('Accept-Encoding', 'identity');
|
|
95
|
+
server.close();
|
|
96
|
+
expect(res.status).toBe(200);
|
|
97
|
+
expect(res.text).toBe('A'.repeat(64));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('file within maxFileSize is served correctly', async () => {
|
|
101
|
+
// maxFileSize: 128 bytes — small.txt (64 bytes) fits
|
|
102
|
+
const server = createApp({
|
|
103
|
+
serverCache: { rawFile: { enabled: true, maxFileSize: 128 } }
|
|
104
|
+
});
|
|
105
|
+
const res = await supertest(server)
|
|
106
|
+
.get('/small.txt')
|
|
107
|
+
.set('Accept-Encoding', 'identity');
|
|
108
|
+
server.close();
|
|
109
|
+
expect(res.status).toBe(200);
|
|
110
|
+
expect(res.text).toBe('A'.repeat(64));
|
|
111
|
+
expect(Number(res.headers['content-length'])).toBe(64);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ─── Cache invalidation ───────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
describe('serverCache.rawFile — cache invalidation on file change', () => {
|
|
118
|
+
let tmpDir;
|
|
119
|
+
let server;
|
|
120
|
+
|
|
121
|
+
beforeAll(() => {
|
|
122
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kcs-cache-inval-'));
|
|
123
|
+
fs.writeFileSync(path.join(tmpDir, 'dynamic.txt'), 'version-1');
|
|
124
|
+
|
|
125
|
+
const app = new Koa();
|
|
126
|
+
app.use(koaClassicServer(tmpDir, {
|
|
127
|
+
showDirContents: false,
|
|
128
|
+
serverCache: { rawFile: { enabled: true } }
|
|
129
|
+
}));
|
|
130
|
+
server = app.listen();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
afterAll(() => {
|
|
134
|
+
server.close();
|
|
135
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('initial content is served correctly', async () => {
|
|
139
|
+
const res = await supertest(server)
|
|
140
|
+
.get('/dynamic.txt')
|
|
141
|
+
.set('Accept-Encoding', 'identity');
|
|
142
|
+
expect(res.status).toBe(200);
|
|
143
|
+
expect(res.text).toBe('version-1');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('updated file content is served after cache invalidation', async () => {
|
|
147
|
+
// Wait 10ms to ensure mtime changes on filesystems with 1ms resolution
|
|
148
|
+
await new Promise(r => setTimeout(r, 10));
|
|
149
|
+
fs.writeFileSync(path.join(tmpDir, 'dynamic.txt'), 'version-2');
|
|
150
|
+
|
|
151
|
+
const res = await supertest(server)
|
|
152
|
+
.get('/dynamic.txt')
|
|
153
|
+
.set('Accept-Encoding', 'identity');
|
|
154
|
+
expect(res.status).toBe(200);
|
|
155
|
+
expect(res.text).toBe('version-2');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ─── rawFile + Range requests ─────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
describe('serverCache.rawFile — Range request served from buffer', () => {
|
|
162
|
+
let server;
|
|
163
|
+
beforeAll(() => {
|
|
164
|
+
server = createApp({ serverCache: { rawFile: { enabled: true } } });
|
|
165
|
+
});
|
|
166
|
+
afterAll(() => server.close());
|
|
167
|
+
|
|
168
|
+
test('Range request returns 206 with correct slice', async () => {
|
|
169
|
+
// Warm cache first
|
|
170
|
+
await supertest(server).get('/small.txt').set('Accept-Encoding', 'identity');
|
|
171
|
+
|
|
172
|
+
const res = await supertest(server)
|
|
173
|
+
.get('/small.txt')
|
|
174
|
+
.set('Range', 'bytes=0-9')
|
|
175
|
+
.set('Accept-Encoding', 'identity');
|
|
176
|
+
expect(res.status).toBe(206);
|
|
177
|
+
expect(res.text).toBe('A'.repeat(10));
|
|
178
|
+
expect(res.headers['content-length']).toBe('10');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('Range request returns correct Content-Range header', async () => {
|
|
182
|
+
const res = await supertest(server)
|
|
183
|
+
.get('/small.txt')
|
|
184
|
+
.set('Range', 'bytes=10-19')
|
|
185
|
+
.set('Accept-Encoding', 'identity');
|
|
186
|
+
expect(res.status).toBe(206);
|
|
187
|
+
expect(res.headers['content-range']).toBe('bytes 10-19/64');
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ─── rawFile + compression ────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
describe('serverCache.rawFile + compression — rawFile feeds compressedFile', () => {
|
|
194
|
+
let server;
|
|
195
|
+
beforeAll(() => {
|
|
196
|
+
server = createApp({
|
|
197
|
+
compression: { minSize: false }, // compress small.txt too
|
|
198
|
+
serverCache: { rawFile: { enabled: true } }
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
afterAll(() => server.close());
|
|
202
|
+
|
|
203
|
+
test('compressed response is served correctly when rawFile cache is warm', async () => {
|
|
204
|
+
// Warm rawFile cache
|
|
205
|
+
await supertest(server).get('/small.txt').set('Accept-Encoding', 'identity');
|
|
206
|
+
|
|
207
|
+
// Request compressed version — rawFile buffer used as compression input
|
|
208
|
+
const res = await supertest(server)
|
|
209
|
+
.get('/small.txt')
|
|
210
|
+
.set('Accept-Encoding', 'gzip');
|
|
211
|
+
expect(res.status).toBe(200);
|
|
212
|
+
expect(res.headers['content-encoding']).toBe('gzip');
|
|
213
|
+
// supertest auto-decompresses gzip
|
|
214
|
+
expect(res.text).toBe('A'.repeat(64));
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ─── compressedFile cache — default enabled ───────────────────────────────────
|
|
219
|
+
|
|
220
|
+
describe('serverCache.compressedFile — enabled by default', () => {
|
|
221
|
+
let server;
|
|
222
|
+
beforeAll(() => { server = createApp(); });
|
|
223
|
+
afterAll(() => server.close());
|
|
224
|
+
|
|
225
|
+
test('large.txt compressed response has Content-Length (buffered, not streamed)', async () => {
|
|
226
|
+
const res = await supertest(server)
|
|
227
|
+
.get('/large.txt')
|
|
228
|
+
.set('Accept-Encoding', 'gzip');
|
|
229
|
+
expect(res.status).toBe(200);
|
|
230
|
+
expect(res.headers['content-encoding']).toBe('gzip');
|
|
231
|
+
expect(res.headers['content-length']).toBeDefined();
|
|
232
|
+
expect(Number(res.headers['content-length'])).toBeGreaterThan(0);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// ─── compressedFile cache — disabled (streaming) ──────────────────────────────
|
|
237
|
+
|
|
238
|
+
describe('serverCache.compressedFile — disabled (streaming mode)', () => {
|
|
239
|
+
let server;
|
|
240
|
+
beforeAll(() => {
|
|
241
|
+
server = createApp({ serverCache: { compressedFile: { enabled: false } } });
|
|
242
|
+
});
|
|
243
|
+
afterAll(() => server.close());
|
|
244
|
+
|
|
245
|
+
test('compressed response has no Content-Length in streaming mode', async () => {
|
|
246
|
+
const res = await supertest(server)
|
|
247
|
+
.get('/large.txt')
|
|
248
|
+
.set('Accept-Encoding', 'gzip');
|
|
249
|
+
expect(res.status).toBe(200);
|
|
250
|
+
expect(res.headers['content-encoding']).toBe('gzip');
|
|
251
|
+
expect(res.headers['content-length']).toBeUndefined();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('streaming body is correctly decompressed', async () => {
|
|
255
|
+
const res = await supertest(server)
|
|
256
|
+
.get('/large.txt')
|
|
257
|
+
.set('Accept-Encoding', 'gzip');
|
|
258
|
+
expect(res.text).toBe('B'.repeat(2048));
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// ─── LFU eviction ────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
describe('serverCache.rawFile — LFU eviction when maxSize exceeded', () => {
|
|
265
|
+
let tmpDir;
|
|
266
|
+
let server;
|
|
267
|
+
|
|
268
|
+
beforeAll(() => {
|
|
269
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kcs-lfu-test-'));
|
|
270
|
+
// fileA: 100 bytes, fileB: 100 bytes — cache maxSize: 150 bytes
|
|
271
|
+
// After caching A (100 bytes) and B (100 bytes), total = 200 > 150.
|
|
272
|
+
// LFU should evict A (hits=1) when B is added if A has fewer hits than B.
|
|
273
|
+
fs.writeFileSync(path.join(tmpDir, 'fileA.txt'), 'A'.repeat(100));
|
|
274
|
+
fs.writeFileSync(path.join(tmpDir, 'fileB.txt'), 'B'.repeat(100));
|
|
275
|
+
fs.writeFileSync(path.join(tmpDir, 'fileC.txt'), 'C'.repeat(100));
|
|
276
|
+
|
|
277
|
+
const app = new Koa();
|
|
278
|
+
app.use(koaClassicServer(tmpDir, {
|
|
279
|
+
showDirContents: false,
|
|
280
|
+
serverCache: {
|
|
281
|
+
rawFile: {
|
|
282
|
+
enabled: true,
|
|
283
|
+
maxSize: 150, // fits 1 file (100 bytes) + half of another
|
|
284
|
+
maxFileSize: 200,
|
|
285
|
+
warnInterval: false, // suppress warnings during test
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}));
|
|
289
|
+
server = app.listen();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
afterAll(() => {
|
|
293
|
+
server.close();
|
|
294
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('all files return correct content even when eviction occurs', async () => {
|
|
298
|
+
// Access A twice (hits=2), then B once (hits=1), then C once
|
|
299
|
+
// When C is added, LFU should evict B (lowest hits among cached entries)
|
|
300
|
+
const resA1 = await supertest(server).get('/fileA.txt').set('Accept-Encoding', 'identity');
|
|
301
|
+
const resA2 = await supertest(server).get('/fileA.txt').set('Accept-Encoding', 'identity');
|
|
302
|
+
const resB = await supertest(server).get('/fileB.txt').set('Accept-Encoding', 'identity');
|
|
303
|
+
const resC = await supertest(server).get('/fileC.txt').set('Accept-Encoding', 'identity');
|
|
304
|
+
|
|
305
|
+
expect(resA1.text).toBe('A'.repeat(100));
|
|
306
|
+
expect(resA2.text).toBe('A'.repeat(100));
|
|
307
|
+
expect(resB.text).toBe('B'.repeat(100));
|
|
308
|
+
expect(resC.text).toBe('C'.repeat(100));
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// ─── warnInterval ────────────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
describe('serverCache.rawFile — warnInterval throttles warnings', () => {
|
|
315
|
+
test('warnInterval: false suppresses all warnings', async () => {
|
|
316
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
317
|
+
|
|
318
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kcs-warn-test-'));
|
|
319
|
+
fs.writeFileSync(path.join(tmpDir, 'a.txt'), 'A'.repeat(100));
|
|
320
|
+
fs.writeFileSync(path.join(tmpDir, 'b.txt'), 'B'.repeat(100));
|
|
321
|
+
|
|
322
|
+
const app = new Koa();
|
|
323
|
+
app.use(koaClassicServer(tmpDir, {
|
|
324
|
+
showDirContents: false,
|
|
325
|
+
serverCache: {
|
|
326
|
+
rawFile: {
|
|
327
|
+
enabled: true,
|
|
328
|
+
maxSize: 50, // too small to fit either file → eviction on every request
|
|
329
|
+
maxFileSize: 200,
|
|
330
|
+
warnInterval: false, // no warnings
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}));
|
|
334
|
+
const server = app.listen();
|
|
335
|
+
|
|
336
|
+
await supertest(server).get('/a.txt').set('Accept-Encoding', 'identity');
|
|
337
|
+
await supertest(server).get('/b.txt').set('Accept-Encoding', 'identity');
|
|
338
|
+
|
|
339
|
+
const cacheWarnings = warnSpy.mock.calls.filter(c =>
|
|
340
|
+
c[0] && c[0].toString().includes('serverCache.rawFile')
|
|
341
|
+
);
|
|
342
|
+
expect(cacheWarnings.length).toBe(0);
|
|
343
|
+
|
|
344
|
+
server.close();
|
|
345
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
346
|
+
warnSpy.mockRestore();
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// ─── rawFile buffer passed to template render ─────────────────────────────────
|
|
351
|
+
|
|
352
|
+
describe('serverCache.rawFile — buffer passed as 4th param to render function', () => {
|
|
353
|
+
let tmpDir;
|
|
354
|
+
let server;
|
|
355
|
+
let capturedBuffer;
|
|
356
|
+
|
|
357
|
+
beforeAll(() => {
|
|
358
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kcs-render-buf-'));
|
|
359
|
+
fs.writeFileSync(path.join(tmpDir, 'page.tmpl'), 'hello from template');
|
|
360
|
+
|
|
361
|
+
const app = new Koa();
|
|
362
|
+
app.use(koaClassicServer(tmpDir, {
|
|
363
|
+
showDirContents: false,
|
|
364
|
+
serverCache: { rawFile: { enabled: true } },
|
|
365
|
+
template: {
|
|
366
|
+
ext: ['tmpl'],
|
|
367
|
+
render: async (ctx, next, filePath, buffer) => {
|
|
368
|
+
capturedBuffer = buffer;
|
|
369
|
+
const content = buffer
|
|
370
|
+
? buffer.toString('utf-8')
|
|
371
|
+
: await fs.promises.readFile(filePath, 'utf-8');
|
|
372
|
+
ctx.type = 'text/plain';
|
|
373
|
+
ctx.body = content;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}));
|
|
377
|
+
server = app.listen();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
afterAll(() => {
|
|
381
|
+
server.close();
|
|
382
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('render receives buffer on first request (cache miss populates buffer)', async () => {
|
|
386
|
+
capturedBuffer = undefined;
|
|
387
|
+
const res = await supertest(server).get('/page.tmpl');
|
|
388
|
+
expect(res.status).toBe(200);
|
|
389
|
+
expect(res.text).toBe('hello from template');
|
|
390
|
+
// Buffer should be populated (cache miss still reads file and caches it)
|
|
391
|
+
expect(Buffer.isBuffer(capturedBuffer)).toBe(true);
|
|
392
|
+
expect(capturedBuffer.toString('utf-8')).toBe('hello from template');
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test('render receives buffer on subsequent request (cache hit)', async () => {
|
|
396
|
+
capturedBuffer = undefined;
|
|
397
|
+
const res = await supertest(server).get('/page.tmpl');
|
|
398
|
+
expect(res.status).toBe(200);
|
|
399
|
+
expect(Buffer.isBuffer(capturedBuffer)).toBe(true);
|
|
400
|
+
expect(capturedBuffer.toString('utf-8')).toBe('hello from template');
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('render receives null when rawFile cache is disabled', async () => {
|
|
404
|
+
let bufferWhenDisabled;
|
|
405
|
+
const appNoCache = new Koa();
|
|
406
|
+
appNoCache.use(koaClassicServer(tmpDir, {
|
|
407
|
+
showDirContents: false,
|
|
408
|
+
// rawFile.enabled: false by default
|
|
409
|
+
template: {
|
|
410
|
+
ext: ['tmpl'],
|
|
411
|
+
render: async (ctx, next, filePath, buffer) => {
|
|
412
|
+
bufferWhenDisabled = buffer;
|
|
413
|
+
ctx.type = 'text/plain';
|
|
414
|
+
ctx.body = 'ok';
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}));
|
|
418
|
+
const s = appNoCache.listen();
|
|
419
|
+
await supertest(s).get('/page.tmpl');
|
|
420
|
+
s.close();
|
|
421
|
+
expect(bufferWhenDisabled).toBeNull();
|
|
422
|
+
});
|
|
423
|
+
});
|
|
@@ -161,8 +161,9 @@ describeIfSymlinks('koa-classic-server - symlink support', () => {
|
|
|
161
161
|
test('GET / serves symlinked index.ejs, not directory listing', async () => {
|
|
162
162
|
const res = await request.get('/');
|
|
163
163
|
expect(res.status).toBe(200);
|
|
164
|
-
|
|
165
|
-
expect(
|
|
164
|
+
const body = res.text !== undefined ? res.text : res.body.toString('utf8');
|
|
165
|
+
expect(body).toContain('EJS via Symlink');
|
|
166
|
+
expect(body).not.toContain('Index of');
|
|
166
167
|
});
|
|
167
168
|
});
|
|
168
169
|
|
|
@@ -187,7 +188,8 @@ describeIfSymlinks('koa-classic-server - symlink support', () => {
|
|
|
187
188
|
test('GET /index.ejs via symlink returns 200', async () => {
|
|
188
189
|
const res = await request.get('/index.ejs');
|
|
189
190
|
expect(res.status).toBe(200);
|
|
190
|
-
|
|
191
|
+
const body = res.text !== undefined ? res.text : res.body.toString('utf8');
|
|
192
|
+
expect(body).toContain('EJS via Symlink');
|
|
191
193
|
});
|
|
192
194
|
|
|
193
195
|
test('GET /linked-style.css via symlink returns 200 with correct mime', async () => {
|
|
@@ -431,8 +433,9 @@ describeIfSymlinks('koa-classic-server - symlink support', () => {
|
|
|
431
433
|
test('GET / finds symlinked index.ejs via RegExp pattern', async () => {
|
|
432
434
|
const res = await request.get('/');
|
|
433
435
|
expect(res.status).toBe(200);
|
|
434
|
-
|
|
435
|
-
expect(
|
|
436
|
+
const body = res.text !== undefined ? res.text : res.body.toString('utf8');
|
|
437
|
+
expect(body).toContain('EJS via Symlink');
|
|
438
|
+
expect(body).not.toContain('Index of');
|
|
436
439
|
});
|
|
437
440
|
});
|
|
438
441
|
});
|