koa-classic-server 2.6.1 → 3.0.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/CLAUDE.md +101 -0
- package/README.md +564 -591
- package/__tests__/benchmark-results-v3.0.0.txt +372 -0
- package/__tests__/benchmark.js +1 -1
- 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 +284 -0
- package/__tests__/customTest/serversToLoad.util.js +5 -5
- package/__tests__/demo-regex-index.js +4 -4
- package/__tests__/deprecation-warnings.test.js +71 -183
- package/__tests__/directory-sorting-links.test.js +1 -1
- package/__tests__/dt-unknown.test.js +39 -28
- package/__tests__/ejs.test.js +1 -1
- 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 +407 -0
- package/__tests__/hideExtension.test.js +70 -13
- package/__tests__/index-option.test.js +18 -16
- package/__tests__/index.test.js +14 -10
- package/__tests__/listing.test.js +437 -0
- package/__tests__/logger.test.js +232 -0
- package/__tests__/range-fixtures/sample.txt +1 -0
- package/__tests__/range.test.js +223 -0
- package/__tests__/security-headers.test.js +165 -0
- package/__tests__/security.test.js +148 -162
- package/__tests__/server-cache-fixtures/large.txt +1 -0
- package/__tests__/server-cache-fixtures/small.txt +1 -0
- package/__tests__/server-cache.test.js +594 -0
- package/__tests__/symlink.test.js +18 -15
- package/__tests__/template-timeout.test.js +321 -0
- package/docs/ACTION_PLAN.md +293 -0
- package/docs/CHANGELOG.md +289 -0
- package/docs/CODE_REVIEW.md +2 -0
- package/docs/DOCUMENTATION.md +259 -32
- package/docs/EXAMPLES_INDEX_OPTION.md +3 -3
- package/docs/FLOW_DIAGRAM.md +15 -13
- package/docs/INDEX_OPTION_PRIORITY.md +2 -2
- package/docs/OPTIMIZATION_HTTP_CACHING.md +2 -0
- package/docs/OPTIMIZATION_ROADMAP_for_V3.md +864 -0
- package/docs/PERFORMANCE_COMPARISON.md +7 -7
- package/docs/security_improvement_for_V3.md +421 -0
- package/docs/template-engine/TEMPLATE_ENGINE_GUIDE.md +5 -5
- package/docs/template-engine/esempi-incrementali.js +1 -1
- package/eslint.config.mjs +17 -0
- package/index.cjs +1507 -429
- package/index.mjs +1 -5
- package/package.json +9 -1
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
const supertest = require('supertest');
|
|
2
|
+
const koaClassicServer = require('../index.cjs');
|
|
3
|
+
const Koa = require('koa');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const root = path.join(__dirname, 'compression-fixtures');
|
|
7
|
+
|
|
8
|
+
// Fixtures:
|
|
9
|
+
// large.txt — 2000 bytes of 'A' (text/plain, exceeds 1KB threshold)
|
|
10
|
+
// small.txt — 4 bytes of 'tiny' (text/plain, below 1KB threshold)
|
|
11
|
+
// data.json — 16 bytes '{"key":"value"}\n' (application/json, below threshold)
|
|
12
|
+
|
|
13
|
+
function createApp(opts = {}) {
|
|
14
|
+
const app = new Koa();
|
|
15
|
+
app.use(koaClassicServer(root, { dirListing: { enabled: false }, ...opts }));
|
|
16
|
+
return app.listen();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ─── Default behaviour (compression enabled, serverCache enabled) ─────────────
|
|
20
|
+
|
|
21
|
+
describe('Compression — default: br preferred', () => {
|
|
22
|
+
let server;
|
|
23
|
+
beforeAll(() => { server = createApp(); });
|
|
24
|
+
afterAll(() => server.close());
|
|
25
|
+
|
|
26
|
+
test('large.txt with Accept-Encoding: br → brotli compressed', async () => {
|
|
27
|
+
const res = await supertest(server)
|
|
28
|
+
.get('/large.txt')
|
|
29
|
+
.set('Accept-Encoding', 'br');
|
|
30
|
+
expect(res.status).toBe(200);
|
|
31
|
+
expect(res.headers['content-encoding']).toBe('br');
|
|
32
|
+
expect(res.headers['vary']).toBe('Accept-Encoding');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('large.txt with Accept-Encoding: gzip → gzip compressed, body decompressed by supertest', async () => {
|
|
36
|
+
const res = await supertest(server)
|
|
37
|
+
.get('/large.txt')
|
|
38
|
+
.set('Accept-Encoding', 'gzip');
|
|
39
|
+
expect(res.status).toBe(200);
|
|
40
|
+
expect(res.headers['content-encoding']).toBe('gzip');
|
|
41
|
+
expect(res.headers['vary']).toBe('Accept-Encoding');
|
|
42
|
+
// supertest auto-decompresses gzip — res.text is the original content
|
|
43
|
+
expect(res.text).toBe('A'.repeat(2000));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('large.txt with Accept-Encoding: br,gzip → br preferred (higher priority)', async () => {
|
|
47
|
+
const res = await supertest(server)
|
|
48
|
+
.get('/large.txt')
|
|
49
|
+
.set('Accept-Encoding', 'br, gzip');
|
|
50
|
+
expect(res.headers['content-encoding']).toBe('br');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Use 'identity' to prevent supertest's default Accept-Encoding from triggering compression
|
|
54
|
+
test('large.txt with Accept-Encoding: identity → uncompressed', async () => {
|
|
55
|
+
const res = await supertest(server)
|
|
56
|
+
.get('/large.txt')
|
|
57
|
+
.set('Accept-Encoding', 'identity');
|
|
58
|
+
expect(res.status).toBe(200);
|
|
59
|
+
expect(res.headers['content-encoding']).toBeUndefined();
|
|
60
|
+
expect(res.headers['vary']).toBeUndefined();
|
|
61
|
+
expect(res.text).toBe('A'.repeat(2000));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('Content-Length present on compressed response (serverCache)', async () => {
|
|
65
|
+
const res = await supertest(server)
|
|
66
|
+
.get('/large.txt')
|
|
67
|
+
.set('Accept-Encoding', 'gzip');
|
|
68
|
+
expect(res.headers['content-length']).toBeDefined();
|
|
69
|
+
expect(Number(res.headers['content-length'])).toBeGreaterThan(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('Compressed Content-Length is smaller than original file size', async () => {
|
|
73
|
+
const res = await supertest(server)
|
|
74
|
+
.get('/large.txt')
|
|
75
|
+
.set('Accept-Encoding', 'gzip');
|
|
76
|
+
expect(Number(res.headers['content-length'])).toBeLessThan(2000);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ─── Threshold: files below threshold are served uncompressed ────────────────
|
|
81
|
+
|
|
82
|
+
describe('Compression — threshold (default 1024 bytes)', () => {
|
|
83
|
+
let server;
|
|
84
|
+
beforeAll(() => { server = createApp(); });
|
|
85
|
+
afterAll(() => server.close());
|
|
86
|
+
|
|
87
|
+
test('small.txt (4 bytes) below threshold → no compression', async () => {
|
|
88
|
+
const res = await supertest(server)
|
|
89
|
+
.get('/small.txt')
|
|
90
|
+
.set('Accept-Encoding', 'br, gzip');
|
|
91
|
+
expect(res.status).toBe(200);
|
|
92
|
+
expect(res.headers['content-encoding']).toBeUndefined();
|
|
93
|
+
expect(res.text).toBe('tiny');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('large.txt (2000 bytes) above threshold → compressed', async () => {
|
|
97
|
+
const res = await supertest(server)
|
|
98
|
+
.get('/large.txt')
|
|
99
|
+
.set('Accept-Encoding', 'gzip');
|
|
100
|
+
expect(res.headers['content-encoding']).toBe('gzip');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('minFileSize: false → compress regardless of size', async () => {
|
|
104
|
+
const s = createApp({ compression: { minFileSize: false } });
|
|
105
|
+
const res = await supertest(s)
|
|
106
|
+
.get('/small.txt')
|
|
107
|
+
.set('Accept-Encoding', 'gzip');
|
|
108
|
+
s.close();
|
|
109
|
+
expect(res.headers['content-encoding']).toBe('gzip');
|
|
110
|
+
// supertest auto-decompresses gzip
|
|
111
|
+
expect(res.text).toBe('tiny');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ─── compression: false shorthand ────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
describe('Compression — disabled', () => {
|
|
118
|
+
let server;
|
|
119
|
+
beforeAll(() => { server = createApp({ compression: false }); });
|
|
120
|
+
afterAll(() => server.close());
|
|
121
|
+
|
|
122
|
+
test('compression: false → no compression on any file', async () => {
|
|
123
|
+
const res = await supertest(server)
|
|
124
|
+
.get('/large.txt')
|
|
125
|
+
.set('Accept-Encoding', 'br, gzip');
|
|
126
|
+
expect(res.status).toBe(200);
|
|
127
|
+
expect(res.headers['content-encoding']).toBeUndefined();
|
|
128
|
+
expect(res.text).toBe('A'.repeat(2000));
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ─── encodings configuration ──────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
describe('Compression — encodings configuration', () => {
|
|
135
|
+
test('encodings: [gzip] → no brotli even if client prefers br', async () => {
|
|
136
|
+
const s = createApp({ compression: { encodings: ['gzip'] } });
|
|
137
|
+
const res = await supertest(s)
|
|
138
|
+
.get('/large.txt')
|
|
139
|
+
.set('Accept-Encoding', 'br, gzip');
|
|
140
|
+
s.close();
|
|
141
|
+
expect(res.headers['content-encoding']).toBe('gzip');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('encodings: [] → no compression', async () => {
|
|
145
|
+
const s = createApp({ compression: { encodings: [] } });
|
|
146
|
+
const res = await supertest(s)
|
|
147
|
+
.get('/large.txt')
|
|
148
|
+
.set('Accept-Encoding', 'br, gzip');
|
|
149
|
+
s.close();
|
|
150
|
+
expect(res.headers['content-encoding']).toBeUndefined();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ─── mimeTypes configuration ──────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
describe('Compression — mimeTypes configuration', () => {
|
|
157
|
+
test('custom mimeTypes replaces default list', async () => {
|
|
158
|
+
// Only compress application/json; text/plain should not be compressed
|
|
159
|
+
const s = createApp({ compression: { mimeTypes: ['application/json'], threshold: false } });
|
|
160
|
+
|
|
161
|
+
const resTxt = await supertest(s)
|
|
162
|
+
.get('/large.txt')
|
|
163
|
+
.set('Accept-Encoding', 'gzip');
|
|
164
|
+
expect(resTxt.headers['content-encoding']).toBeUndefined();
|
|
165
|
+
|
|
166
|
+
s.close();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ─── ETag encoding-specific ───────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
describe('Compression — encoding-specific ETag', () => {
|
|
173
|
+
let server;
|
|
174
|
+
beforeAll(() => { server = createApp({ browserCacheEnabled: true }); });
|
|
175
|
+
afterAll(() => server.close());
|
|
176
|
+
|
|
177
|
+
test('ETag for br response has -br suffix', async () => {
|
|
178
|
+
const res = await supertest(server)
|
|
179
|
+
.get('/large.txt')
|
|
180
|
+
.set('Accept-Encoding', 'br');
|
|
181
|
+
expect(res.headers['etag']).toMatch(/-br"$/);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('ETag for gzip response has -gz suffix', async () => {
|
|
185
|
+
const res = await supertest(server)
|
|
186
|
+
.get('/large.txt')
|
|
187
|
+
.set('Accept-Encoding', 'gzip');
|
|
188
|
+
expect(res.headers['etag']).toMatch(/-gz"$/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('ETag for uncompressed response has no suffix', async () => {
|
|
192
|
+
// Use 'identity' to prevent supertest's default Accept-Encoding from triggering compression
|
|
193
|
+
const res = await supertest(server)
|
|
194
|
+
.get('/large.txt')
|
|
195
|
+
.set('Accept-Encoding', 'identity');
|
|
196
|
+
expect(res.headers['etag']).not.toMatch(/-(br|gz)"$/);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('304 returned when If-None-Match matches encoding-specific ETag', async () => {
|
|
200
|
+
const first = await supertest(server)
|
|
201
|
+
.get('/large.txt')
|
|
202
|
+
.set('Accept-Encoding', 'gzip');
|
|
203
|
+
const etag = first.headers['etag'];
|
|
204
|
+
|
|
205
|
+
const second = await supertest(server)
|
|
206
|
+
.get('/large.txt')
|
|
207
|
+
.set('Accept-Encoding', 'gzip')
|
|
208
|
+
.set('If-None-Match', etag);
|
|
209
|
+
expect(second.status).toBe(304);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('304 NOT returned when ETag suffix differs (br ETag sent with gzip request)', async () => {
|
|
213
|
+
const brRes = await supertest(server)
|
|
214
|
+
.get('/large.txt')
|
|
215
|
+
.set('Accept-Encoding', 'br');
|
|
216
|
+
const brEtag = brRes.headers['etag'];
|
|
217
|
+
|
|
218
|
+
// Send the br ETag but request gzip → ETag mismatch → 200
|
|
219
|
+
const gzipRes = await supertest(server)
|
|
220
|
+
.get('/large.txt')
|
|
221
|
+
.set('Accept-Encoding', 'gzip')
|
|
222
|
+
.set('If-None-Match', brEtag);
|
|
223
|
+
expect(gzipRes.status).toBe(200);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ─── serverCache.compressedFile disabled (streaming mode) ────────────────────
|
|
228
|
+
|
|
229
|
+
describe('Compression — serverCache.compressedFile disabled (streaming)', () => {
|
|
230
|
+
let server;
|
|
231
|
+
beforeAll(() => {
|
|
232
|
+
server = createApp({ serverCache: { compressedFile: { enabled: false } } });
|
|
233
|
+
});
|
|
234
|
+
afterAll(() => server.close());
|
|
235
|
+
|
|
236
|
+
test('streaming: Content-Encoding set but no Content-Length', async () => {
|
|
237
|
+
const res = await supertest(server)
|
|
238
|
+
.get('/large.txt')
|
|
239
|
+
.set('Accept-Encoding', 'gzip');
|
|
240
|
+
expect(res.headers['content-encoding']).toBe('gzip');
|
|
241
|
+
// Streaming compressed responses use Transfer-Encoding: chunked → no Content-Length
|
|
242
|
+
expect(res.headers['content-length']).toBeUndefined();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('streaming: response body is correctly decompressed', async () => {
|
|
246
|
+
const res = await supertest(server)
|
|
247
|
+
.get('/large.txt')
|
|
248
|
+
.set('Accept-Encoding', 'gzip');
|
|
249
|
+
// supertest auto-decompresses gzip — res.text is the original content
|
|
250
|
+
expect(res.text).toBe('A'.repeat(2000));
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ─── Compression does not apply to Range requests ────────────────────────────
|
|
255
|
+
|
|
256
|
+
describe('Compression — no compression on Range requests (HTTP 206)', () => {
|
|
257
|
+
let server;
|
|
258
|
+
beforeAll(() => { server = createApp(); });
|
|
259
|
+
afterAll(() => server.close());
|
|
260
|
+
|
|
261
|
+
test('Range request is served uncompressed even with Accept-Encoding', async () => {
|
|
262
|
+
const res = await supertest(server)
|
|
263
|
+
.get('/large.txt')
|
|
264
|
+
.set('Range', 'bytes=0-9')
|
|
265
|
+
.set('Accept-Encoding', 'br, gzip');
|
|
266
|
+
expect(res.status).toBe(206);
|
|
267
|
+
expect(res.headers['content-encoding']).toBeUndefined();
|
|
268
|
+
expect(res.text).toBe('A'.repeat(10));
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// ─── V3 migration guard ───────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
describe('Compression — V3 migration guard (old name throws helpful error)', () => {
|
|
275
|
+
test('compression.minSize throws with hint pointing to minFileSize', () => {
|
|
276
|
+
expect(() => koaClassicServer(root, { compression: { minSize: 2048 } }))
|
|
277
|
+
.toThrow(/options\.compression\.minSize was renamed[\s\S]*compression: \{ minFileSize: 2048 \}/);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('compression.minSize === false also throws (legacy "no minimum" sentinel)', () => {
|
|
281
|
+
expect(() => koaClassicServer(root, { compression: { minSize: false } }))
|
|
282
|
+
.toThrow(/options\.compression\.minSize was renamed/);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
@@ -20,7 +20,7 @@ const configurations = [
|
|
|
20
20
|
options: {
|
|
21
21
|
//urlPrefix: '/',
|
|
22
22
|
method: ['GET'],
|
|
23
|
-
|
|
23
|
+
dirListing: { enabled: true },
|
|
24
24
|
},
|
|
25
25
|
},
|
|
26
26
|
{
|
|
@@ -31,8 +31,8 @@ const configurations = [
|
|
|
31
31
|
options: {
|
|
32
32
|
urlPrefix: '/public',
|
|
33
33
|
method: ['GET'],
|
|
34
|
-
|
|
35
|
-
index: 'index.html',
|
|
34
|
+
dirListing: { enabled: true },
|
|
35
|
+
index: ['index.html'],
|
|
36
36
|
},
|
|
37
37
|
},
|
|
38
38
|
{
|
|
@@ -43,7 +43,7 @@ const configurations = [
|
|
|
43
43
|
options: {
|
|
44
44
|
urlPrefix: '/public',
|
|
45
45
|
method: ['GET'],
|
|
46
|
-
|
|
46
|
+
dirListing: { enabled: true },
|
|
47
47
|
},
|
|
48
48
|
},
|
|
49
49
|
{
|
|
@@ -54,7 +54,7 @@ const configurations = [
|
|
|
54
54
|
options: {
|
|
55
55
|
urlPrefix: '',
|
|
56
56
|
method: ['GET'],
|
|
57
|
-
|
|
57
|
+
dirListing: { enabled: true },
|
|
58
58
|
urlsReserved : Array('/percorso_riservato', '/percorso riservato con spazi')
|
|
59
59
|
},
|
|
60
60
|
},
|
|
@@ -48,7 +48,7 @@ console.log(' Aspettativa: Matcha INDEX.HTML o Index.Html');
|
|
|
48
48
|
const app1 = new Koa();
|
|
49
49
|
app1.use(koaClassicServer(testDir, {
|
|
50
50
|
index: [/index\.html/i],
|
|
51
|
-
|
|
51
|
+
dirListing: { enabled: true }
|
|
52
52
|
}));
|
|
53
53
|
const server1 = app1.listen(3001);
|
|
54
54
|
console.log(' ✓ Server avviato su http://localhost:3001\n');
|
|
@@ -63,7 +63,7 @@ console.log(' Aspettativa: Matcha INDEX.HTML, Index.Html, index.htm');
|
|
|
63
63
|
const app2 = new Koa();
|
|
64
64
|
app2.use(koaClassicServer(testDir, {
|
|
65
65
|
index: [/index\.(html|htm)/i],
|
|
66
|
-
|
|
66
|
+
dirListing: { enabled: true }
|
|
67
67
|
}));
|
|
68
68
|
const server2 = app2.listen(3002);
|
|
69
69
|
console.log(' ✓ Server avviato su http://localhost:3002\n');
|
|
@@ -81,7 +81,7 @@ app3.use(koaClassicServer(testDir, {
|
|
|
81
81
|
/index\.(html|htm)/i,
|
|
82
82
|
/default\.html/i
|
|
83
83
|
],
|
|
84
|
-
|
|
84
|
+
dirListing: { enabled: true }
|
|
85
85
|
}));
|
|
86
86
|
const server3 = app3.listen(3003);
|
|
87
87
|
console.log(' ✓ Server avviato su http://localhost:3003\n');
|
|
@@ -100,7 +100,7 @@ app4.use(koaClassicServer(testDir, {
|
|
|
100
100
|
/INDEX\.HTML/i, // Case-insensitive fallback
|
|
101
101
|
/default\.html/i // Default fallback
|
|
102
102
|
],
|
|
103
|
-
|
|
103
|
+
dirListing: { enabled: true }
|
|
104
104
|
}));
|
|
105
105
|
const server4 = app4.listen(3004);
|
|
106
106
|
console.log(' ✓ Server avviato su http://localhost:3004\n');
|
|
@@ -1,217 +1,105 @@
|
|
|
1
1
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
2
2
|
//
|
|
3
|
-
// TEST FOR
|
|
4
|
-
//
|
|
3
|
+
// TEST FOR REMOVED OPTION NAMES (enableCaching, cacheMaxAge)
|
|
4
|
+
// These options were removed in v3.0.0 — passing them must throw an Error.
|
|
5
5
|
//
|
|
6
6
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
7
7
|
|
|
8
|
-
const supertest = require('supertest');
|
|
9
8
|
const koaClassicServer = require('../index.cjs');
|
|
10
9
|
const Koa = require('koa');
|
|
11
10
|
const path = require('path');
|
|
12
11
|
|
|
13
12
|
const rootDir = path.join(__dirname, 'publicWwwTest');
|
|
14
13
|
|
|
15
|
-
describe('
|
|
14
|
+
describe('Removed option names (v3.0.0 breaking changes)', () => {
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
beforeAll(() => {
|
|
23
|
-
// Spy on console.warn to capture deprecation warnings
|
|
24
|
-
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
25
|
-
|
|
26
|
-
app = new Koa();
|
|
27
|
-
|
|
28
|
-
// Use deprecated option name
|
|
29
|
-
app.use(koaClassicServer(rootDir, {
|
|
30
|
-
enableCaching: true // DEPRECATED: should use browserCacheEnabled
|
|
31
|
-
}));
|
|
32
|
-
|
|
33
|
-
server = app.listen();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
afterAll(() => {
|
|
37
|
-
server.close();
|
|
38
|
-
consoleWarnSpy.mockRestore();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('should display deprecation warning for enableCaching', () => {
|
|
42
|
-
expect(consoleWarnSpy).toHaveBeenCalled();
|
|
43
|
-
const warningMessage = consoleWarnSpy.mock.calls[0][1];
|
|
44
|
-
expect(warningMessage).toContain('DEPRECATION WARNING');
|
|
45
|
-
expect(warningMessage).toContain('enableCaching');
|
|
46
|
-
expect(warningMessage).toContain('browserCacheEnabled');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('should still work with deprecated option', async () => {
|
|
50
|
-
const response = await supertest(server).get('/test-page.html');
|
|
51
|
-
|
|
52
|
-
// Should return the file
|
|
53
|
-
expect(response.status).toBe(200);
|
|
54
|
-
|
|
55
|
-
// Should have caching headers (because enableCaching: true was set)
|
|
56
|
-
expect(response.headers['etag']).toBeDefined();
|
|
57
|
-
expect(response.headers['last-modified']).toBeDefined();
|
|
58
|
-
expect(response.headers['cache-control']).toContain('public');
|
|
59
|
-
});
|
|
16
|
+
test('should throw when "enableCaching" is passed', () => {
|
|
17
|
+
expect(() => {
|
|
18
|
+
const app = new Koa();
|
|
19
|
+
app.use(koaClassicServer(rootDir, { enableCaching: true }));
|
|
20
|
+
}).toThrow('"enableCaching" option was removed in v3.0.0');
|
|
60
21
|
});
|
|
61
22
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
beforeAll(() => {
|
|
68
|
-
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
69
|
-
|
|
70
|
-
app = new Koa();
|
|
71
|
-
|
|
72
|
-
// Use deprecated option name
|
|
73
|
-
app.use(koaClassicServer(rootDir, {
|
|
74
|
-
enableCaching: true, // Also deprecated
|
|
75
|
-
cacheMaxAge: 7200 // DEPRECATED: should use browserCacheMaxAge
|
|
76
|
-
}));
|
|
77
|
-
|
|
78
|
-
server = app.listen();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
afterAll(() => {
|
|
82
|
-
server.close();
|
|
83
|
-
consoleWarnSpy.mockRestore();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('should display deprecation warning for cacheMaxAge', () => {
|
|
87
|
-
const warningCalls = consoleWarnSpy.mock.calls;
|
|
88
|
-
const cacheMaxAgeWarning = warningCalls.find(call =>
|
|
89
|
-
call[1] && call[1].includes('cacheMaxAge')
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
expect(cacheMaxAgeWarning).toBeDefined();
|
|
93
|
-
expect(cacheMaxAgeWarning[1]).toContain('DEPRECATION WARNING');
|
|
94
|
-
expect(cacheMaxAgeWarning[1]).toContain('browserCacheMaxAge');
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test('should use the deprecated cacheMaxAge value', async () => {
|
|
98
|
-
const response = await supertest(server).get('/test-page.html');
|
|
99
|
-
|
|
100
|
-
expect(response.status).toBe(200);
|
|
101
|
-
expect(response.headers['cache-control']).toContain('max-age=7200');
|
|
102
|
-
});
|
|
23
|
+
test('should throw when "cacheMaxAge" is passed', () => {
|
|
24
|
+
expect(() => {
|
|
25
|
+
const app = new Koa();
|
|
26
|
+
app.use(koaClassicServer(rootDir, { cacheMaxAge: 3600 }));
|
|
27
|
+
}).toThrow('"cacheMaxAge" option was removed in v3.0.0');
|
|
103
28
|
});
|
|
104
29
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
beforeAll(() => {
|
|
111
|
-
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
112
|
-
|
|
113
|
-
app = new Koa();
|
|
114
|
-
|
|
115
|
-
// Use NEW option names
|
|
116
|
-
app.use(koaClassicServer(rootDir, {
|
|
117
|
-
browserCacheEnabled: true,
|
|
118
|
-
browserCacheMaxAge: 3600
|
|
119
|
-
}));
|
|
120
|
-
|
|
121
|
-
server = app.listen();
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
afterAll(() => {
|
|
125
|
-
server.close();
|
|
126
|
-
consoleWarnSpy.mockRestore();
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test('should NOT display deprecation warnings', () => {
|
|
130
|
-
// Filter out warnings from other tests (like index option deprecation)
|
|
131
|
-
const cachingWarnings = consoleWarnSpy.mock.calls.filter(call =>
|
|
132
|
-
call[1] && (call[1].includes('enableCaching') || call[1].includes('cacheMaxAge'))
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
expect(cachingWarnings.length).toBe(0);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test('should work correctly with new option names', async () => {
|
|
139
|
-
const response = await supertest(server).get('/test-page.html');
|
|
140
|
-
|
|
141
|
-
expect(response.status).toBe(200);
|
|
142
|
-
expect(response.headers['etag']).toBeDefined();
|
|
143
|
-
expect(response.headers['cache-control']).toContain('max-age=3600');
|
|
144
|
-
});
|
|
30
|
+
test('should throw even when new option is also provided alongside "cacheMaxAge"', () => {
|
|
31
|
+
expect(() => {
|
|
32
|
+
const app = new Koa();
|
|
33
|
+
app.use(koaClassicServer(rootDir, { cacheMaxAge: 3600, browserCacheMaxAge: 3600 }));
|
|
34
|
+
}).toThrow('"cacheMaxAge" option was removed in v3.0.0');
|
|
145
35
|
});
|
|
146
36
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
app = new Koa();
|
|
37
|
+
test('should throw even when new option is also provided alongside "enableCaching"', () => {
|
|
38
|
+
expect(() => {
|
|
39
|
+
const app = new Koa();
|
|
40
|
+
app.use(koaClassicServer(rootDir, { enableCaching: false, browserCacheEnabled: false }));
|
|
41
|
+
}).toThrow('"enableCaching" option was removed in v3.0.0');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
156
44
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
browserCacheMaxAge: 9999 // NEW (should take precedence)
|
|
163
|
-
}));
|
|
45
|
+
describe('New option names (browserCacheEnabled / browserCacheMaxAge)', () => {
|
|
46
|
+
const supertest = require('supertest');
|
|
47
|
+
let app;
|
|
48
|
+
let server;
|
|
49
|
+
let consoleWarnSpy;
|
|
164
50
|
|
|
165
|
-
|
|
166
|
-
});
|
|
51
|
+
beforeAll(() => {
|
|
52
|
+
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
167
53
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
54
|
+
app = new Koa();
|
|
55
|
+
app.use(koaClassicServer(rootDir, {
|
|
56
|
+
browserCacheEnabled: true,
|
|
57
|
+
browserCacheMaxAge: 3600
|
|
58
|
+
}));
|
|
172
59
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const cachingWarnings = consoleWarnSpy.mock.calls.filter(call =>
|
|
176
|
-
call[1] && (call[1].includes('enableCaching') || call[1].includes('cacheMaxAge'))
|
|
177
|
-
);
|
|
60
|
+
server = app.listen();
|
|
61
|
+
});
|
|
178
62
|
|
|
179
|
-
|
|
180
|
-
|
|
63
|
+
afterAll(() => {
|
|
64
|
+
server.close();
|
|
65
|
+
consoleWarnSpy.mockRestore();
|
|
66
|
+
});
|
|
181
67
|
|
|
182
|
-
|
|
183
|
-
|
|
68
|
+
test('should not warn about removed options', () => {
|
|
69
|
+
const removedWarnings = consoleWarnSpy.mock.calls.filter(call =>
|
|
70
|
+
call[1] && (call[1].includes('enableCaching') || call[1].includes('cacheMaxAge'))
|
|
71
|
+
);
|
|
72
|
+
expect(removedWarnings.length).toBe(0);
|
|
73
|
+
});
|
|
184
74
|
|
|
185
|
-
|
|
75
|
+
test('should work correctly with new option names', async () => {
|
|
76
|
+
const response = await supertest(server).get('/test-page.html');
|
|
186
77
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
expect(response.headers['cache-control']).toContain('no-cache');
|
|
191
|
-
});
|
|
78
|
+
expect(response.status).toBe(200);
|
|
79
|
+
expect(response.headers['etag']).toBeDefined();
|
|
80
|
+
expect(response.headers['cache-control']).toContain('max-age=3600');
|
|
192
81
|
});
|
|
82
|
+
});
|
|
193
83
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
beforeAll(() => {
|
|
199
|
-
app = new Koa();
|
|
200
|
-
app.use(koaClassicServer(rootDir));
|
|
201
|
-
server = app.listen();
|
|
202
|
-
});
|
|
84
|
+
describe('Default behavior (no caching options specified)', () => {
|
|
85
|
+
const supertest = require('supertest');
|
|
86
|
+
let app;
|
|
87
|
+
let server;
|
|
203
88
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
89
|
+
beforeAll(() => {
|
|
90
|
+
app = new Koa();
|
|
91
|
+
app.use(koaClassicServer(rootDir));
|
|
92
|
+
server = app.listen();
|
|
93
|
+
});
|
|
207
94
|
|
|
208
|
-
|
|
209
|
-
|
|
95
|
+
afterAll(() => {
|
|
96
|
+
server.close();
|
|
97
|
+
});
|
|
210
98
|
|
|
211
|
-
|
|
99
|
+
test('should default to browserCacheEnabled: false', async () => {
|
|
100
|
+
const response = await supertest(server).get('/test-page.html');
|
|
212
101
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
});
|
|
102
|
+
expect(response.status).toBe(200);
|
|
103
|
+
expect(response.headers['cache-control']).toContain('no-cache');
|
|
216
104
|
});
|
|
217
105
|
});
|