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,422 @@
|
|
|
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, 'hidden-fixtures');
|
|
7
|
+
|
|
8
|
+
function createApp(hiddenOpts) {
|
|
9
|
+
const app = new Koa();
|
|
10
|
+
app.use(koaClassicServer(root, { showDirContents: true, hidden: hiddenOpts }));
|
|
11
|
+
return app.listen();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ─── Implicit default warning ─────────────────────────────────────────────────
|
|
15
|
+
// This describe block MUST be first: the warning flag is module-level and fires
|
|
16
|
+
// only once per Jest worker process. Jest isolates each test FILE into its own
|
|
17
|
+
// module registry, so the flag starts as false when this file loads.
|
|
18
|
+
|
|
19
|
+
describe('hidden option — implicit default warning', () => {
|
|
20
|
+
let warnSpy;
|
|
21
|
+
|
|
22
|
+
beforeAll(() => {
|
|
23
|
+
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterAll(() => {
|
|
27
|
+
warnSpy.mockRestore();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('warns when dotFiles.default and dotDirs.default are not explicitly set', () => {
|
|
31
|
+
warnSpy.mockClear();
|
|
32
|
+
koaClassicServer(root, {}); // no hidden config at all — both defaults implicit
|
|
33
|
+
const warnings = warnSpy.mock.calls.filter(c =>
|
|
34
|
+
c[1] && c[1].includes('dotFiles') && c[1].includes('dotDirs')
|
|
35
|
+
);
|
|
36
|
+
expect(warnings.length).toBe(1);
|
|
37
|
+
expect(warnings[0][1]).toContain('hidden.dotFiles.default');
|
|
38
|
+
expect(warnings[0][1]).toContain('v3.0.0');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('does not warn again on subsequent calls (once per process)', () => {
|
|
42
|
+
warnSpy.mockClear();
|
|
43
|
+
koaClassicServer(root, {}); // flag already set from the test above
|
|
44
|
+
const warnings = warnSpy.mock.calls.filter(c =>
|
|
45
|
+
c[1] && c[1].includes('dotFiles')
|
|
46
|
+
);
|
|
47
|
+
expect(warnings.length).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('no warning when both dotFiles.default and dotDirs.default are explicitly set', () => {
|
|
51
|
+
// Use jest.isolateModules to obtain a fresh copy of index.cjs whose flag is false,
|
|
52
|
+
// then verify that an explicit configuration emits no warning.
|
|
53
|
+
let fresh;
|
|
54
|
+
jest.isolateModules(() => {
|
|
55
|
+
fresh = require('../index.cjs');
|
|
56
|
+
});
|
|
57
|
+
warnSpy.mockClear();
|
|
58
|
+
fresh(root, {
|
|
59
|
+
hidden: {
|
|
60
|
+
dotFiles: { default: 'hidden' },
|
|
61
|
+
dotDirs: { default: 'visible' },
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const warnings = warnSpy.mock.calls.filter(c =>
|
|
65
|
+
c[1] && c[1].includes('dotFiles')
|
|
66
|
+
);
|
|
67
|
+
expect(warnings.length).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ─── Option validation ────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
describe('hidden option — validation', () => {
|
|
74
|
+
test('throws when dotFiles.default is not "hidden" or "visible"', () => {
|
|
75
|
+
expect(() =>
|
|
76
|
+
koaClassicServer(root, { hidden: { dotFiles: { default: 'yes' } } })
|
|
77
|
+
).toThrow(/hidden\.dotFiles\.default must be "hidden" or "visible"/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('throws when dotDirs.default is not "hidden" or "visible"', () => {
|
|
81
|
+
expect(() =>
|
|
82
|
+
koaClassicServer(root, { hidden: { dotDirs: { default: 'yes' } } })
|
|
83
|
+
).toThrow(/hidden\.dotDirs\.default must be "hidden" or "visible"/);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('does not throw when hidden option is omitted', () => {
|
|
87
|
+
expect(() => koaClassicServer(root, {})).not.toThrow();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('does not throw when hidden is a valid object', () => {
|
|
91
|
+
expect(() =>
|
|
92
|
+
koaClassicServer(root, {
|
|
93
|
+
hidden: {
|
|
94
|
+
dotFiles: { default: 'hidden', whitelist: ['.well-known'], blacklist: ['.env'] },
|
|
95
|
+
dotDirs: { default: 'visible', blacklist: [/^\.git/] },
|
|
96
|
+
alwaysHide: ['*.secret', /\.key$/]
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
).not.toThrow();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ─── dotFiles default: 'hidden' (system default) ─────────────────────────────
|
|
104
|
+
|
|
105
|
+
describe('dotFiles — default hidden (system default)', () => {
|
|
106
|
+
let server;
|
|
107
|
+
beforeAll(() => { server = createApp(undefined); });
|
|
108
|
+
afterAll(() => server.close());
|
|
109
|
+
|
|
110
|
+
test('GET /.env returns 404', async () => {
|
|
111
|
+
const res = await supertest(server).get('/.env');
|
|
112
|
+
expect(res.status).toBe(404);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('GET /.gitignore returns 404', async () => {
|
|
116
|
+
const res = await supertest(server).get('/.gitignore');
|
|
117
|
+
expect(res.status).toBe(404);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('GET /subdir/.env returns 404 (dot-file in subdirectory)', async () => {
|
|
121
|
+
const res = await supertest(server).get('/subdir/.env');
|
|
122
|
+
expect(res.status).toBe(404);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('directory listing does not include .env', async () => {
|
|
126
|
+
const res = await supertest(server).get('/');
|
|
127
|
+
expect(res.status).toBe(200);
|
|
128
|
+
expect(res.text).not.toContain('.env');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('directory listing does not include .gitignore', async () => {
|
|
132
|
+
const res = await supertest(server).get('/');
|
|
133
|
+
expect(res.text).not.toContain('.gitignore');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('regular files remain accessible', async () => {
|
|
137
|
+
const res = await supertest(server).get('/normal.txt');
|
|
138
|
+
expect(res.status).toBe(200);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ─── dotFiles default: 'visible' ─────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
describe('dotFiles — default visible', () => {
|
|
145
|
+
let server;
|
|
146
|
+
beforeAll(() => {
|
|
147
|
+
server = createApp({ dotFiles: { default: 'visible' } });
|
|
148
|
+
});
|
|
149
|
+
afterAll(() => server.close());
|
|
150
|
+
|
|
151
|
+
test('GET /.env returns 200 when dotFiles.default is "visible"', async () => {
|
|
152
|
+
const res = await supertest(server).get('/.env');
|
|
153
|
+
expect(res.status).toBe(200);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('directory listing includes .env when dotFiles.default is "visible"', async () => {
|
|
157
|
+
const res = await supertest(server).get('/');
|
|
158
|
+
expect(res.text).toContain('.env');
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ─── dotFiles whitelist ───────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
describe('dotFiles — whitelist exceptions', () => {
|
|
165
|
+
let server;
|
|
166
|
+
beforeAll(() => {
|
|
167
|
+
server = createApp({
|
|
168
|
+
dotFiles: { default: 'hidden', whitelist: ['.well-known'] },
|
|
169
|
+
dotDirs: { default: 'hidden', whitelist: ['.well-known'] }
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
afterAll(() => server.close());
|
|
173
|
+
|
|
174
|
+
test('GET /.well-known/ is accessible (whitelisted dir)', async () => {
|
|
175
|
+
const res = await supertest(server).get('/.well-known/');
|
|
176
|
+
expect(res.status).toBe(200);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('GET /.well-known/acme-challenge.txt is accessible (inside whitelisted dir)', async () => {
|
|
180
|
+
const res = await supertest(server).get('/.well-known/acme-challenge.txt');
|
|
181
|
+
expect(res.status).toBe(200);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('.well-known appears in root listing', async () => {
|
|
185
|
+
const res = await supertest(server).get('/');
|
|
186
|
+
expect(res.text).toContain('.well-known');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('GET /.env still returns 404 (not whitelisted)', async () => {
|
|
190
|
+
const res = await supertest(server).get('/.env');
|
|
191
|
+
expect(res.status).toBe(404);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('whitelist with RegExp: /^\\.public/ matches .public-assets', async () => {
|
|
195
|
+
// This tests the regex matching logic; .public-assets doesn't exist so we
|
|
196
|
+
// expect 404 from "not found", not from "hidden" — indirectly confirms regex is not blocking
|
|
197
|
+
const server2 = createApp({
|
|
198
|
+
dotFiles: { default: 'hidden', whitelist: [/^\.public/] }
|
|
199
|
+
});
|
|
200
|
+
// .env is not matched by /^\.public/ so it stays hidden
|
|
201
|
+
const res = await supertest(server2).get('/.env');
|
|
202
|
+
expect(res.status).toBe(404);
|
|
203
|
+
server2.close();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ─── dotFiles blacklist ───────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
describe('dotFiles — blacklist', () => {
|
|
210
|
+
let server;
|
|
211
|
+
beforeAll(() => {
|
|
212
|
+
server = createApp({
|
|
213
|
+
dotFiles: { default: 'visible', blacklist: ['.env'] }
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
afterAll(() => server.close());
|
|
217
|
+
|
|
218
|
+
test('GET /.env returns 404 (blacklisted even with default: visible)', async () => {
|
|
219
|
+
const res = await supertest(server).get('/.env');
|
|
220
|
+
expect(res.status).toBe(404);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('GET /.gitignore returns 200 (visible, not blacklisted)', async () => {
|
|
224
|
+
const res = await supertest(server).get('/.gitignore');
|
|
225
|
+
expect(res.status).toBe(200);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('.env not in listing when blacklisted', async () => {
|
|
229
|
+
const res = await supertest(server).get('/');
|
|
230
|
+
expect(res.text).not.toContain('.env');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ─── blacklist beats whitelist ────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
describe('dotFiles — blacklist beats whitelist', () => {
|
|
237
|
+
let server;
|
|
238
|
+
beforeAll(() => {
|
|
239
|
+
server = createApp({
|
|
240
|
+
dotFiles: { default: 'visible', whitelist: ['.env'], blacklist: ['.env'] }
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
afterAll(() => server.close());
|
|
244
|
+
|
|
245
|
+
test('GET /.env returns 404 (blacklist wins over whitelist)', async () => {
|
|
246
|
+
const res = await supertest(server).get('/.env');
|
|
247
|
+
expect(res.status).toBe(404);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// ─── dotDirs default: 'visible' (system default) ─────────────────────────────
|
|
252
|
+
|
|
253
|
+
describe('dotDirs — default visible (system default)', () => {
|
|
254
|
+
let server;
|
|
255
|
+
beforeAll(() => { server = createApp(undefined); });
|
|
256
|
+
afterAll(() => server.close());
|
|
257
|
+
|
|
258
|
+
test('.dot-dir appears in root listing by default', async () => {
|
|
259
|
+
const res = await supertest(server).get('/');
|
|
260
|
+
expect(res.text).toContain('.dot-dir');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('GET /.dot-dir/ returns 200 (directory listing) by default', async () => {
|
|
264
|
+
const res = await supertest(server).get('/.dot-dir/');
|
|
265
|
+
expect(res.status).toBe(200);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('GET /.dot-dir/inside.txt returns 200 by default', async () => {
|
|
269
|
+
const res = await supertest(server).get('/.dot-dir/inside.txt');
|
|
270
|
+
expect(res.status).toBe(200);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// ─── dotDirs blacklist ────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
describe('dotDirs — blacklist', () => {
|
|
277
|
+
let server;
|
|
278
|
+
beforeAll(() => {
|
|
279
|
+
server = createApp({ dotDirs: { blacklist: ['.dot-dir'] } });
|
|
280
|
+
});
|
|
281
|
+
afterAll(() => server.close());
|
|
282
|
+
|
|
283
|
+
test('GET /.dot-dir/ returns 404 (blacklisted dir)', async () => {
|
|
284
|
+
const res = await supertest(server).get('/.dot-dir/');
|
|
285
|
+
expect(res.status).toBe(404);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('GET /.dot-dir/inside.txt returns 404 (inside blocked dir)', async () => {
|
|
289
|
+
const res = await supertest(server).get('/.dot-dir/inside.txt');
|
|
290
|
+
expect(res.status).toBe(404);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('.dot-dir not in root listing when blacklisted', async () => {
|
|
294
|
+
const res = await supertest(server).get('/');
|
|
295
|
+
expect(res.text).not.toContain('.dot-dir');
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// ─── dotDirs blacklist with RegExp ────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
describe('dotDirs — blacklist with RegExp', () => {
|
|
302
|
+
let server;
|
|
303
|
+
beforeAll(() => {
|
|
304
|
+
server = createApp({ dotDirs: { blacklist: [/^\.dot/] } });
|
|
305
|
+
});
|
|
306
|
+
afterAll(() => server.close());
|
|
307
|
+
|
|
308
|
+
test('GET /.dot-dir/ returns 404 (RegExp blacklist match)', async () => {
|
|
309
|
+
const res = await supertest(server).get('/.dot-dir/');
|
|
310
|
+
expect(res.status).toBe(404);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('.well-known accessible (does not match /^\\.dot/)', async () => {
|
|
314
|
+
const res = await supertest(server).get('/.well-known/');
|
|
315
|
+
expect(res.status).toBe(200);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// ─── alwaysHide ───────────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
describe('alwaysHide — glob and regex patterns', () => {
|
|
322
|
+
let server;
|
|
323
|
+
beforeAll(() => {
|
|
324
|
+
server = createApp({
|
|
325
|
+
dotFiles: { default: 'visible' }, // dot-files visible so we can test alwaysHide separately
|
|
326
|
+
alwaysHide: ['*.secret', /\.key$/]
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
afterAll(() => server.close());
|
|
330
|
+
|
|
331
|
+
test('GET /file.secret returns 404 (glob *.secret)', async () => {
|
|
332
|
+
const res = await supertest(server).get('/file.secret');
|
|
333
|
+
expect(res.status).toBe(404);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('GET /data.key returns 404 (regex /\\.key$/)', async () => {
|
|
337
|
+
const res = await supertest(server).get('/data.key');
|
|
338
|
+
expect(res.status).toBe(404);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('file.secret not in directory listing', async () => {
|
|
342
|
+
const res = await supertest(server).get('/');
|
|
343
|
+
expect(res.text).not.toContain('file.secret');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('data.key not in directory listing', async () => {
|
|
347
|
+
const res = await supertest(server).get('/');
|
|
348
|
+
expect(res.text).not.toContain('data.key');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test('regular files remain accessible', async () => {
|
|
352
|
+
const res = await supertest(server).get('/normal.txt');
|
|
353
|
+
expect(res.status).toBe(200);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ─── alwaysHide — path-anchored patterns ─────────────────────────────────────
|
|
358
|
+
|
|
359
|
+
describe('alwaysHide — path-anchored glob (config/secrets/**)', () => {
|
|
360
|
+
let server;
|
|
361
|
+
beforeAll(() => {
|
|
362
|
+
server = createApp({ alwaysHide: ['config/secrets/**'] });
|
|
363
|
+
});
|
|
364
|
+
afterAll(() => server.close());
|
|
365
|
+
|
|
366
|
+
test('GET /config/secrets/password.txt returns 404', async () => {
|
|
367
|
+
const res = await supertest(server).get('/config/secrets/password.txt');
|
|
368
|
+
expect(res.status).toBe(404);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('password.txt not in /config/secrets/ listing', async () => {
|
|
372
|
+
const res = await supertest(server).get('/config/secrets/');
|
|
373
|
+
expect(res.status).toBe(200);
|
|
374
|
+
expect(res.text).not.toContain('password.txt');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test('GET /normal.txt remains accessible (not under config/secrets/)', async () => {
|
|
378
|
+
const res = await supertest(server).get('/normal.txt');
|
|
379
|
+
expect(res.status).toBe(200);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// ─── alwaysHide secondary to whitelist ───────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
describe('alwaysHide — secondary to dotFiles whitelist', () => {
|
|
386
|
+
let server;
|
|
387
|
+
beforeAll(() => {
|
|
388
|
+
server = createApp({
|
|
389
|
+
dotFiles: { default: 'hidden', whitelist: ['.env'] },
|
|
390
|
+
alwaysHide: ['.env'] // both alwaysHide and whitelist target .env
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
afterAll(() => server.close());
|
|
394
|
+
|
|
395
|
+
test('GET /.env returns 200 (whitelist wins over alwaysHide)', async () => {
|
|
396
|
+
const res = await supertest(server).get('/.env');
|
|
397
|
+
expect(res.status).toBe(200);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// ─── deep tree: dot-files hidden at any depth ─────────────────────────────────
|
|
402
|
+
|
|
403
|
+
describe('hidden entries at any depth in directory tree', () => {
|
|
404
|
+
let server;
|
|
405
|
+
beforeAll(() => { server = createApp(undefined); });
|
|
406
|
+
afterAll(() => server.close());
|
|
407
|
+
|
|
408
|
+
test('GET /subdir/.env returns 404 (dot-file hidden at any depth)', async () => {
|
|
409
|
+
const res = await supertest(server).get('/subdir/.env');
|
|
410
|
+
expect(res.status).toBe(404);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test('/subdir/.env not in /subdir/ listing', async () => {
|
|
414
|
+
const res = await supertest(server).get('/subdir/');
|
|
415
|
+
expect(res.text).not.toContain('.env');
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test('GET /subdir/regular.txt returns 200 (regular file accessible)', async () => {
|
|
419
|
+
const res = await supertest(server).get('/subdir/regular.txt');
|
|
420
|
+
expect(res.status).toBe(200);
|
|
421
|
+
});
|
|
422
|
+
});
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Enhanced Index Option Tests
|
|
3
3
|
*
|
|
4
|
-
* Tests for the
|
|
5
|
-
* - String (backward compatible)
|
|
4
|
+
* Tests for the index option that supports:
|
|
6
5
|
* - Array of strings
|
|
7
6
|
* - Array of RegExp
|
|
8
7
|
* - Mixed array (strings + RegExp)
|
|
9
8
|
* - Priority handling (first match wins)
|
|
9
|
+
*
|
|
10
|
+
* v3.0.0: string format removed — passing a non-empty string throws an Error.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
const Koa = require('koa');
|
|
@@ -39,21 +40,20 @@ describe('Enhanced Index Option Tests', () => {
|
|
|
39
40
|
}
|
|
40
41
|
});
|
|
41
42
|
|
|
42
|
-
describe('
|
|
43
|
-
test('
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
app.use(koaClassicServer(tempDir, { index: 'index.html' }));
|
|
49
|
-
server = app.listen();
|
|
43
|
+
describe('String index — removed in v3.0.0', () => {
|
|
44
|
+
test('Non-empty string should throw an Error', () => {
|
|
45
|
+
expect(() => {
|
|
46
|
+
new Koa().use(koaClassicServer(tempDir, { index: 'index.html' }));
|
|
47
|
+
}).toThrow('"index" option no longer accepts a string in v3.0.0');
|
|
48
|
+
});
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
expect(
|
|
53
|
-
|
|
50
|
+
test('Throw message should include migration hint', () => {
|
|
51
|
+
expect(() => {
|
|
52
|
+
new Koa().use(koaClassicServer(tempDir, { index: 'default.htm' }));
|
|
53
|
+
}).toThrow('index: ["default.htm"]');
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
test('Empty string should show directory listing', async () => {
|
|
56
|
+
test('Empty string should show directory listing (no throw)', async () => {
|
|
57
57
|
fs.writeFileSync(path.join(tempDir, 'test.txt'), 'test');
|
|
58
58
|
|
|
59
59
|
app = new Koa();
|
|
@@ -281,7 +281,8 @@ describe('Enhanced Index Option Tests', () => {
|
|
|
281
281
|
|
|
282
282
|
const res = await supertest(server).get('/');
|
|
283
283
|
expect(res.status).toBe(200);
|
|
284
|
-
|
|
284
|
+
const body = res.text !== undefined ? res.text : res.body.toString('utf8');
|
|
285
|
+
expect(body).toContain('EJS content');
|
|
285
286
|
});
|
|
286
287
|
});
|
|
287
288
|
|
|
@@ -362,7 +363,8 @@ describe('Enhanced Index Option Tests', () => {
|
|
|
362
363
|
|
|
363
364
|
const res = await supertest(server).get('/');
|
|
364
365
|
expect(res.status).toBe(200);
|
|
365
|
-
|
|
366
|
+
const body = res.text !== undefined ? res.text : res.body.toString('utf8');
|
|
367
|
+
expect(body).toContain('pug content');
|
|
366
368
|
});
|
|
367
369
|
|
|
368
370
|
test('Case-insensitive filesystem (Windows-like): matches any case', async () => {
|
package/__tests__/index.test.js
CHANGED
|
@@ -115,7 +115,7 @@ describe(` koaClassicServer options1: ${JSON.stringify(options1)}`, () => {
|
|
|
115
115
|
const options2 = {
|
|
116
116
|
method: ['GET'],
|
|
117
117
|
showDirContents: false,
|
|
118
|
-
index: 'index.html',
|
|
118
|
+
index: ['index.html'],
|
|
119
119
|
};
|
|
120
120
|
|
|
121
121
|
describe(` koaClassicServer options2: ${JSON.stringify(options2)}`, () => {
|
|
@@ -213,7 +213,7 @@ function getFilesRecursivelySync(dir) {
|
|
|
213
213
|
results = results.concat(getFilesRecursivelySync(fullPath));
|
|
214
214
|
} else if (entry.isFile()) {
|
|
215
215
|
// Se l'entry è un file, lo aggiungiamo all'array dei risultati
|
|
216
|
-
const mimeType = mime.lookup(entry.name) || '
|
|
216
|
+
const mimeType = mime.lookup(entry.name) || 'application/octet-stream'; // fallback for unknown MIME types
|
|
217
217
|
entry.type = 'file';
|
|
218
218
|
entry.mimeType = mimeType;
|
|
219
219
|
results.push(entry);
|
|
@@ -263,7 +263,10 @@ function testAllPathByFileList(filesAndDirArray, getServer, options) {
|
|
|
263
263
|
expect(res.status).toBe(200);
|
|
264
264
|
const content = fs.readFileSync(entry.fullPath, 'utf8');
|
|
265
265
|
expect(res.type).toBe(entry.mimeType);
|
|
266
|
-
|
|
266
|
+
// For binary content-types (e.g. application/octet-stream), supertest populates
|
|
267
|
+
// res.body (Buffer) instead of res.text — normalise to string for comparison
|
|
268
|
+
const responseBody = res.text !== undefined ? res.text : res.body.toString('utf8');
|
|
269
|
+
expect(responseBody).toBe(content);
|
|
267
270
|
} else {//è una directory
|
|
268
271
|
if( options.showDirContents === false ){
|
|
269
272
|
// FIX: Quando directory listing è disabilitato, restituisce 404
|
|
@@ -328,7 +331,8 @@ function testAllPathByFileList(filesAndDirArray, getServer, options) {
|
|
|
328
331
|
if (entry.type === 'file') {
|
|
329
332
|
const content = fs.readFileSync(entry.fullPath, 'utf8');
|
|
330
333
|
expect(res.type).toBe(entry.mimeType);
|
|
331
|
-
|
|
334
|
+
const responseBody = res.text !== undefined ? res.text : res.body.toString('utf8');
|
|
335
|
+
expect(responseBody).toBe(content);
|
|
332
336
|
} else {
|
|
333
337
|
expect(res.type).toBe('text/html');
|
|
334
338
|
expect(res.text).toContain('<!DOCTYPE html>');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0123456789abcdefghij
|