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.
Files changed (40) hide show
  1. package/README.md +68 -10
  2. package/__tests__/caching-headers.test.js +30 -30
  3. package/__tests__/compression-fixtures/data.json +1 -0
  4. package/__tests__/compression-fixtures/large.txt +1 -0
  5. package/__tests__/compression-fixtures/small.txt +1 -0
  6. package/__tests__/compression.test.js +270 -0
  7. package/__tests__/customTest/serversToLoad.util.js +1 -1
  8. package/__tests__/deprecation-warnings.test.js +71 -183
  9. package/__tests__/dt-unknown.test.js +635 -0
  10. package/__tests__/hidden-fixtures/.dot-dir/inside.txt +1 -0
  11. package/__tests__/hidden-fixtures/.env +2 -0
  12. package/__tests__/hidden-fixtures/.well-known/acme-challenge.txt +1 -0
  13. package/__tests__/hidden-fixtures/config/secrets/password.txt +1 -0
  14. package/__tests__/hidden-fixtures/data.key +1 -0
  15. package/__tests__/hidden-fixtures/file.secret +1 -0
  16. package/__tests__/hidden-fixtures/index.html +1 -0
  17. package/__tests__/hidden-fixtures/normal.txt +1 -0
  18. package/__tests__/hidden-fixtures/subdir/.env +1 -0
  19. package/__tests__/hidden-fixtures/subdir/regular.txt +1 -0
  20. package/__tests__/hidden-option.test.js +422 -0
  21. package/__tests__/index-option.test.js +18 -16
  22. package/__tests__/index.test.js +8 -4
  23. package/__tests__/range-fixtures/sample.txt +1 -0
  24. package/__tests__/range.test.js +223 -0
  25. package/__tests__/security-headers.test.js +153 -0
  26. package/__tests__/security.test.js +145 -159
  27. package/__tests__/server-cache-fixtures/large.txt +1 -0
  28. package/__tests__/server-cache-fixtures/small.txt +1 -0
  29. package/__tests__/server-cache.test.js +423 -0
  30. package/__tests__/symlink.test.js +8 -5
  31. package/docs/ACTION_PLAN.md +293 -0
  32. package/docs/CHANGELOG.md +118 -0
  33. package/docs/EXAMPLES_INDEX_OPTION.md +2 -2
  34. package/docs/FLOW_DIAGRAM.md +13 -13
  35. package/docs/OPTIMIZATION_ROADMAP_for_V3.md +864 -0
  36. package/docs/PERFORMANCE_COMPARISON.md +7 -7
  37. package/eslint.config.mjs +17 -0
  38. package/index.cjs +1114 -378
  39. package/index.mjs +1 -5
  40. package/package.json +4 -1
@@ -0,0 +1,635 @@
1
+ /**
2
+ * DT_UNKNOWN filesystem support tests for koa-classic-server
3
+ *
4
+ * Context: On filesystems where readdir({ withFileTypes: true }) returns dirents
5
+ * with DT_UNKNOWN (UV_DIRENT_UNKNOWN = 0), all dirent.is*() methods return false.
6
+ * This occurs on:
7
+ * - NixOS with buildFHSEnv (chroot-like environment for Playwright e2e tests)
8
+ * - overlayfs (used by Docker for image layers)
9
+ * - some FUSE filesystems (sshfs, s3fs, rclone mount)
10
+ * - NFS (some implementations don't support d_type)
11
+ * - ecryptfs (encrypted home directories on Linux)
12
+ *
13
+ * The fix adds a stat() fallback in isFileOrSymlinkToFile(), isDirOrSymlinkToDir(),
14
+ * and show_dir() when the dirent type is unknown (type 0).
15
+ *
16
+ * On standard filesystems (ext4, btrfs, xfs, APFS, NTFS), d_type is always filled
17
+ * correctly, so the stat() fallback is never reached — zero overhead.
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
+ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', () => {
28
+ let tmpDir;
29
+
30
+ beforeAll(() => {
31
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kcs-dt-unknown-test-'));
32
+
33
+ // Create real files and directories that stat() can resolve
34
+ fs.writeFileSync(
35
+ path.join(tmpDir, 'index.html'),
36
+ '<html><head><title>DT_UNKNOWN Index</title></head><body>Hello from DT_UNKNOWN</body></html>'
37
+ );
38
+ fs.writeFileSync(
39
+ path.join(tmpDir, 'index.ejs'),
40
+ '<html><head><title>EJS DT_UNKNOWN</title></head><body><h1>EJS Works</h1></body></html>'
41
+ );
42
+ fs.writeFileSync(
43
+ path.join(tmpDir, 'style.css'),
44
+ 'body { color: blue; }'
45
+ );
46
+ fs.writeFileSync(
47
+ path.join(tmpDir, 'readme.txt'),
48
+ 'This is a readme file.'
49
+ );
50
+ fs.mkdirSync(path.join(tmpDir, 'subdir'));
51
+ fs.writeFileSync(
52
+ path.join(tmpDir, 'subdir', 'nested.txt'),
53
+ 'nested content'
54
+ );
55
+ });
56
+
57
+ afterAll(() => {
58
+ fs.rmSync(tmpDir, { recursive: true, force: true });
59
+ });
60
+
61
+ /**
62
+ * Helper: create a Dirent with DT_UNKNOWN (type 0).
63
+ * Node.js 18+: new fs.Dirent(name, 0)
64
+ */
65
+ function createUnknownDirent(name) {
66
+ return new fs.Dirent(name, 0);
67
+ }
68
+
69
+ /**
70
+ * Helper: mock fs.promises.readdir to return DT_UNKNOWN dirents for a specific directory.
71
+ * stat() continues to work normally, so the fallback can resolve actual types.
72
+ */
73
+ function mockReaddirWithDtUnknown(targetDir, fileNames) {
74
+ const originalReaddir = fs.promises.readdir;
75
+ const spy = jest.spyOn(fs.promises, 'readdir').mockImplementation(async (dirPath, options) => {
76
+ const resolvedTarget = path.resolve(targetDir);
77
+ const resolvedDir = path.resolve(dirPath);
78
+ if (resolvedDir === resolvedTarget && options && options.withFileTypes) {
79
+ // Return DT_UNKNOWN dirents for all entries
80
+ return fileNames.map(name => createUnknownDirent(name));
81
+ }
82
+ // Fall through to original for other directories
83
+ return originalReaddir.call(fs.promises, dirPath, options);
84
+ });
85
+ return spy;
86
+ }
87
+
88
+ // =========================================================================
89
+ // 1. isFileOrSymlinkToFile with DT_UNKNOWN
90
+ // =========================================================================
91
+ describe('isFileOrSymlinkToFile with DT_UNKNOWN', () => {
92
+ test('should return true for DT_UNKNOWN entry pointing to a regular file', async () => {
93
+ // Use findIndexFile as a proxy to test isFileOrSymlinkToFile behavior
94
+ // When index.html has type 0, it should still be found via stat() fallback
95
+ const spy = mockReaddirWithDtUnknown(tmpDir, ['index.html', 'style.css', 'subdir']);
96
+
97
+ const app = new Koa();
98
+ app.use(koaClassicServer(tmpDir, {
99
+ index: ['index.html'],
100
+ showDirContents: true
101
+ }));
102
+ const server = app.listen();
103
+ const request = supertest(server);
104
+
105
+ try {
106
+ const res = await request.get('/');
107
+ expect(res.status).toBe(200);
108
+ expect(res.text).toContain('DT_UNKNOWN Index');
109
+ expect(res.text).not.toContain('Index of');
110
+ } finally {
111
+ server.close();
112
+ spy.mockRestore();
113
+ }
114
+ });
115
+
116
+ test('should return false for DT_UNKNOWN entry pointing to a directory', async () => {
117
+ // A directory called "subdir" with DT_UNKNOWN should NOT match as an index file
118
+ const spy = mockReaddirWithDtUnknown(tmpDir, ['subdir']);
119
+
120
+ const app = new Koa();
121
+ app.use(koaClassicServer(tmpDir, {
122
+ index: ['subdir'],
123
+ showDirContents: true
124
+ }));
125
+ const server = app.listen();
126
+ const request = supertest(server);
127
+
128
+ try {
129
+ const res = await request.get('/');
130
+ expect(res.status).toBe(200);
131
+ // Should show directory listing since "subdir" is a directory, not a file
132
+ expect(res.text).toContain('Index of');
133
+ } finally {
134
+ server.close();
135
+ spy.mockRestore();
136
+ }
137
+ });
138
+
139
+ test('should return false for DT_UNKNOWN entry pointing to nothing (broken)', async () => {
140
+ // Mock readdir to include a non-existent file
141
+ const spy = mockReaddirWithDtUnknown(tmpDir, ['nonexistent.html', 'index.html']);
142
+
143
+ const app = new Koa();
144
+ app.use(koaClassicServer(tmpDir, {
145
+ index: ['nonexistent.html'],
146
+ showDirContents: true
147
+ }));
148
+ const server = app.listen();
149
+ const request = supertest(server);
150
+
151
+ try {
152
+ const res = await request.get('/');
153
+ expect(res.status).toBe(200);
154
+ // nonexistent.html can't be stat'd, so should show directory listing
155
+ expect(res.text).toContain('Index of');
156
+ } finally {
157
+ server.close();
158
+ spy.mockRestore();
159
+ }
160
+ });
161
+ });
162
+
163
+ // =========================================================================
164
+ // 2. isDirOrSymlinkToDir with DT_UNKNOWN
165
+ // =========================================================================
166
+ describe('isDirOrSymlinkToDir with DT_UNKNOWN', () => {
167
+ test('should return true for DT_UNKNOWN entry pointing to a directory', async () => {
168
+ // Test that isDirOrSymlinkToDir resolves DT_UNKNOWN dirs correctly
169
+ // by checking that findIndexFile does NOT treat a directory as an index file
170
+ const spy = mockReaddirWithDtUnknown(tmpDir, ['subdir', 'index.html']);
171
+
172
+ const app = new Koa();
173
+ app.use(koaClassicServer(tmpDir, {
174
+ index: ['index.html'],
175
+ showDirContents: true
176
+ }));
177
+ const server = app.listen();
178
+ const request = supertest(server);
179
+
180
+ try {
181
+ const res = await request.get('/');
182
+ expect(res.status).toBe(200);
183
+ // index.html (file) should be served, not subdir (directory)
184
+ expect(res.text).toContain('DT_UNKNOWN Index');
185
+ expect(res.text).not.toContain('Index of');
186
+ } finally {
187
+ server.close();
188
+ spy.mockRestore();
189
+ }
190
+ });
191
+
192
+ test('should return false for DT_UNKNOWN entry pointing to a regular file', async () => {
193
+ // Files should NOT be treated as directories.
194
+ // Use a RegExp index pattern to force the readdir-based slow-path in
195
+ // findIndexFile (the string fast-path uses stat() directly and would bypass
196
+ // the mock). The regex never matches, so no index file is found and the
197
+ // directory listing is shown, confirming the DT_UNKNOWN files are treated
198
+ // as regular files (not directories).
199
+ const spy = mockReaddirWithDtUnknown(tmpDir, ['style.css', 'readme.txt']);
200
+
201
+ const app = new Koa();
202
+ app.use(koaClassicServer(tmpDir, {
203
+ index: [/NOMATCH_SENTINEL/],
204
+ showDirContents: true
205
+ }));
206
+ const server = app.listen();
207
+ const request = supertest(server);
208
+
209
+ try {
210
+ const res = await request.get('/');
211
+ expect(res.status).toBe(200);
212
+ // No index.html in the mocked readdir results, so directory listing
213
+ expect(res.text).toContain('Index of');
214
+ } finally {
215
+ server.close();
216
+ spy.mockRestore();
217
+ }
218
+ });
219
+ });
220
+
221
+ // =========================================================================
222
+ // 3. findIndexFile with DT_UNKNOWN entries
223
+ // =========================================================================
224
+ describe('findIndexFile with DT_UNKNOWN entries', () => {
225
+ test('should find index.html when all dirents have DT_UNKNOWN type', async () => {
226
+ const spy = mockReaddirWithDtUnknown(tmpDir, ['style.css', 'index.html', 'readme.txt', 'subdir']);
227
+
228
+ const app = new Koa();
229
+ app.use(koaClassicServer(tmpDir, {
230
+ index: ['index.html'],
231
+ showDirContents: true
232
+ }));
233
+ const server = app.listen();
234
+ const request = supertest(server);
235
+
236
+ try {
237
+ const res = await request.get('/');
238
+ expect(res.status).toBe(200);
239
+ expect(res.text).toContain('DT_UNKNOWN Index');
240
+ expect(res.text).not.toContain('Index of');
241
+ } finally {
242
+ server.close();
243
+ spy.mockRestore();
244
+ }
245
+ });
246
+
247
+ test('should find index.ejs via string pattern when type is unknown', async () => {
248
+ const spy = mockReaddirWithDtUnknown(tmpDir, ['index.ejs', 'style.css', 'subdir']);
249
+
250
+ const app = new Koa();
251
+ app.use(koaClassicServer(tmpDir, {
252
+ index: ['index.ejs'],
253
+ showDirContents: true
254
+ }));
255
+ const server = app.listen();
256
+ const request = supertest(server);
257
+
258
+ try {
259
+ const res = await request.get('/');
260
+ expect(res.status).toBe(200);
261
+ const body = res.text !== undefined ? res.text : res.body.toString('utf8');
262
+ expect(body).toContain('EJS DT_UNKNOWN');
263
+ expect(body).not.toContain('Index of');
264
+ } finally {
265
+ server.close();
266
+ spy.mockRestore();
267
+ }
268
+ });
269
+
270
+ test('should find index.ejs via RegExp pattern when type is unknown', async () => {
271
+ const spy = mockReaddirWithDtUnknown(tmpDir, ['index.ejs', 'style.css', 'subdir']);
272
+
273
+ const app = new Koa();
274
+ app.use(koaClassicServer(tmpDir, {
275
+ index: [/index\.[eE][jJ][sS]/],
276
+ showDirContents: true
277
+ }));
278
+ const server = app.listen();
279
+ const request = supertest(server);
280
+
281
+ try {
282
+ const res = await request.get('/');
283
+ expect(res.status).toBe(200);
284
+ const body = res.text !== undefined ? res.text : res.body.toString('utf8');
285
+ expect(body).toContain('EJS DT_UNKNOWN');
286
+ expect(body).not.toContain('Index of');
287
+ } finally {
288
+ server.close();
289
+ spy.mockRestore();
290
+ }
291
+ });
292
+
293
+ test('should not find index in directory with only subdirectories (all DT_UNKNOWN)', async () => {
294
+ // Use a RegExp to force the readdir-based slow-path (the string fast-path
295
+ // would stat() index.html directly, finding the real file on disk).
296
+ // The regex never matches, so the mocked readdir result (['subdir']) is used:
297
+ // 'subdir' has DT_UNKNOWN → stat fallback → isDirectory() → not a file →
298
+ // fileNames stays empty → no index found → directory listing shown.
299
+ const spy = mockReaddirWithDtUnknown(tmpDir, ['subdir']);
300
+
301
+ const app = new Koa();
302
+ app.use(koaClassicServer(tmpDir, {
303
+ index: [/NOMATCH_SENTINEL/],
304
+ showDirContents: true
305
+ }));
306
+ const server = app.listen();
307
+ const request = supertest(server);
308
+
309
+ try {
310
+ const res = await request.get('/');
311
+ expect(res.status).toBe(200);
312
+ // No file matches the pattern, so directory listing should appear
313
+ expect(res.text).toContain('Index of');
314
+ } finally {
315
+ server.close();
316
+ spy.mockRestore();
317
+ }
318
+ });
319
+ });
320
+
321
+ // =========================================================================
322
+ // 4. show_dir with DT_UNKNOWN entries
323
+ // =========================================================================
324
+ describe('show_dir with DT_UNKNOWN entries', () => {
325
+ test('should list files with DT_UNKNOWN as their resolved type (FILE/DIR)', async () => {
326
+ const spy = mockReaddirWithDtUnknown(tmpDir, ['style.css', 'readme.txt', 'subdir']);
327
+
328
+ const app = new Koa();
329
+ app.use(koaClassicServer(tmpDir, {
330
+ index: [],
331
+ showDirContents: true
332
+ }));
333
+ const server = app.listen();
334
+ const request = supertest(server);
335
+
336
+ try {
337
+ const res = await request.get('/');
338
+ expect(res.status).toBe(200);
339
+ expect(res.text).toContain('Index of');
340
+ // All three entries should appear in the listing
341
+ expect(res.text).toContain('style.css');
342
+ expect(res.text).toContain('readme.txt');
343
+ expect(res.text).toContain('subdir');
344
+ // subdir should be resolved as DIR
345
+ expect(res.text).toMatch(/subdir[\s\S]*?DIR/);
346
+ } finally {
347
+ server.close();
348
+ spy.mockRestore();
349
+ }
350
+ });
351
+
352
+ test('should not skip entries or log "Unknown file type: 0"', async () => {
353
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
354
+ const spy = mockReaddirWithDtUnknown(tmpDir, ['style.css', 'readme.txt', 'subdir']);
355
+
356
+ const app = new Koa();
357
+ app.use(koaClassicServer(tmpDir, {
358
+ index: [],
359
+ showDirContents: true
360
+ }));
361
+ const server = app.listen();
362
+ const request = supertest(server);
363
+
364
+ try {
365
+ const res = await request.get('/');
366
+ expect(res.status).toBe(200);
367
+ // All entries should be present (not skipped)
368
+ expect(res.text).toContain('style.css');
369
+ expect(res.text).toContain('readme.txt');
370
+ expect(res.text).toContain('subdir');
371
+ // Should NOT have logged "Unknown file type: 0"
372
+ const unknownTypeCalls = consoleSpy.mock.calls.filter(
373
+ call => call[0] === 'Unknown file type:' && call[1] === 0
374
+ );
375
+ expect(unknownTypeCalls).toHaveLength(0);
376
+ } finally {
377
+ server.close();
378
+ spy.mockRestore();
379
+ consoleSpy.mockRestore();
380
+ }
381
+ });
382
+
383
+ test('should show correct mime types for DT_UNKNOWN files', async () => {
384
+ const spy = mockReaddirWithDtUnknown(tmpDir, ['style.css', 'readme.txt']);
385
+
386
+ const app = new Koa();
387
+ app.use(koaClassicServer(tmpDir, {
388
+ index: [],
389
+ showDirContents: true
390
+ }));
391
+ const server = app.listen();
392
+ const request = supertest(server);
393
+
394
+ try {
395
+ const res = await request.get('/');
396
+ expect(res.status).toBe(200);
397
+ // CSS file should show text/css mime type
398
+ expect(res.text).toMatch(/style\.css[\s\S]*?text\/css/);
399
+ // TXT file should show text/plain mime type
400
+ expect(res.text).toMatch(/readme\.txt[\s\S]*?text\/plain/);
401
+ } finally {
402
+ server.close();
403
+ spy.mockRestore();
404
+ }
405
+ });
406
+
407
+ test('should show correct sizes for DT_UNKNOWN files', async () => {
408
+ const spy = mockReaddirWithDtUnknown(tmpDir, ['style.css', 'readme.txt']);
409
+
410
+ const app = new Koa();
411
+ app.use(koaClassicServer(tmpDir, {
412
+ index: [],
413
+ showDirContents: true
414
+ }));
415
+ const server = app.listen();
416
+ const request = supertest(server);
417
+
418
+ try {
419
+ const res = await request.get('/');
420
+ expect(res.status).toBe(200);
421
+ // Files should have size values (not '-' which would indicate skipped/broken)
422
+ // style.css is 21 bytes = "21 B"
423
+ expect(res.text).toMatch(/style\.css[\s\S]*?\d+\s*B/);
424
+ // readme.txt is 22 bytes = "22 B"
425
+ expect(res.text).toMatch(/readme\.txt[\s\S]*?\d+\s*B/);
426
+ } finally {
427
+ server.close();
428
+ spy.mockRestore();
429
+ }
430
+ });
431
+ });
432
+
433
+ // =========================================================================
434
+ // 5. Integration: full request with DT_UNKNOWN filesystem
435
+ // =========================================================================
436
+ describe('integration: full request with DT_UNKNOWN filesystem', () => {
437
+ test('GET / serves index file instead of directory listing', async () => {
438
+ const spy = mockReaddirWithDtUnknown(tmpDir, ['index.html', 'style.css', 'readme.txt', 'subdir']);
439
+
440
+ const app = new Koa();
441
+ app.use(koaClassicServer(tmpDir, {
442
+ index: ['index.html'],
443
+ showDirContents: true
444
+ }));
445
+ const server = app.listen();
446
+ const request = supertest(server);
447
+
448
+ try {
449
+ const res = await request.get('/');
450
+ expect(res.status).toBe(200);
451
+ expect(res.text).toContain('DT_UNKNOWN Index');
452
+ expect(res.text).toContain('Hello from DT_UNKNOWN');
453
+ expect(res.text).not.toContain('Index of');
454
+ } finally {
455
+ server.close();
456
+ spy.mockRestore();
457
+ }
458
+ });
459
+
460
+ test('GET /somefile.txt serves the file with 200', async () => {
461
+ // Direct file access uses stat() at the top level, so it works
462
+ // regardless of DT_UNKNOWN — this verifies the direct path still works
463
+ const spy = mockReaddirWithDtUnknown(tmpDir, ['readme.txt', 'style.css', 'subdir']);
464
+
465
+ const app = new Koa();
466
+ app.use(koaClassicServer(tmpDir, {
467
+ index: [],
468
+ showDirContents: true
469
+ }));
470
+ const server = app.listen();
471
+ const request = supertest(server);
472
+
473
+ try {
474
+ const res = await request.get('/readme.txt');
475
+ expect(res.status).toBe(200);
476
+ expect(res.text).toContain('This is a readme file.');
477
+ } finally {
478
+ server.close();
479
+ spy.mockRestore();
480
+ }
481
+ });
482
+
483
+ test('directory listing shows all entries correctly', async () => {
484
+ const spy = mockReaddirWithDtUnknown(tmpDir, ['index.html', 'style.css', 'readme.txt', 'subdir', 'index.ejs']);
485
+
486
+ const app = new Koa();
487
+ app.use(koaClassicServer(tmpDir, {
488
+ index: [],
489
+ showDirContents: true
490
+ }));
491
+ const server = app.listen();
492
+ const request = supertest(server);
493
+
494
+ try {
495
+ const res = await request.get('/');
496
+ expect(res.status).toBe(200);
497
+ expect(res.text).toContain('Index of');
498
+
499
+ // All 5 entries should be listed
500
+ expect(res.text).toContain('index.html');
501
+ expect(res.text).toContain('style.css');
502
+ expect(res.text).toContain('readme.txt');
503
+ expect(res.text).toContain('subdir');
504
+ expect(res.text).toContain('index.ejs');
505
+
506
+ // subdir should show as DIR
507
+ expect(res.text).toMatch(/subdir[\s\S]*?DIR/);
508
+
509
+ // Files should have their correct mime types
510
+ expect(res.text).toMatch(/style\.css[\s\S]*?text\/css/);
511
+ expect(res.text).toMatch(/readme\.txt[\s\S]*?text\/plain/);
512
+ expect(res.text).toMatch(/index\.html[\s\S]*?text\/html/);
513
+ } finally {
514
+ server.close();
515
+ spy.mockRestore();
516
+ }
517
+ });
518
+
519
+ test('GET / with EJS template engine and DT_UNKNOWN still serves index', async () => {
520
+ const spy = mockReaddirWithDtUnknown(tmpDir, ['index.ejs', 'style.css', 'subdir']);
521
+
522
+ const app = new Koa();
523
+ app.use(koaClassicServer(tmpDir, {
524
+ index: ['index.ejs'],
525
+ showDirContents: true,
526
+ template: {
527
+ ext: ['ejs'],
528
+ render: async (ctx, next, filePath) => {
529
+ const content = await fs.promises.readFile(filePath, 'utf-8');
530
+ ctx.type = 'text/html';
531
+ ctx.body = content;
532
+ }
533
+ }
534
+ }));
535
+ const server = app.listen();
536
+ const request = supertest(server);
537
+
538
+ try {
539
+ const res = await request.get('/');
540
+ expect(res.status).toBe(200);
541
+ expect(res.headers['content-type']).toMatch(/html/);
542
+ expect(res.text).toContain('EJS DT_UNKNOWN');
543
+ expect(res.text).not.toContain('Index of');
544
+ } finally {
545
+ server.close();
546
+ spy.mockRestore();
547
+ }
548
+ });
549
+ });
550
+
551
+ // =========================================================================
552
+ // 6. Edge cases
553
+ // =========================================================================
554
+ describe('edge cases', () => {
555
+ test('mixed regular dirents and DT_UNKNOWN dirents work together', async () => {
556
+ // Only mock readdir for the specific directory, verify normal files
557
+ // still work alongside DT_UNKNOWN entries
558
+ const originalReaddir = fs.promises.readdir;
559
+ const spy = jest.spyOn(fs.promises, 'readdir').mockImplementation(async (dirPath, options) => {
560
+ const resolvedTarget = path.resolve(tmpDir);
561
+ const resolvedDir = path.resolve(dirPath);
562
+ if (resolvedDir === resolvedTarget && options && options.withFileTypes) {
563
+ // Mix: some regular (type 1), some DT_UNKNOWN (type 0)
564
+ return [
565
+ new fs.Dirent('index.html', 1), // Regular file
566
+ new fs.Dirent('style.css', 0), // DT_UNKNOWN
567
+ new fs.Dirent('subdir', 0), // DT_UNKNOWN
568
+ ];
569
+ }
570
+ return originalReaddir.call(fs.promises, dirPath, options);
571
+ });
572
+
573
+ const app = new Koa();
574
+ app.use(koaClassicServer(tmpDir, {
575
+ index: [],
576
+ showDirContents: true
577
+ }));
578
+ const server = app.listen();
579
+ const request = supertest(server);
580
+
581
+ try {
582
+ const res = await request.get('/');
583
+ expect(res.status).toBe(200);
584
+ expect(res.text).toContain('Index of');
585
+ // All entries should appear
586
+ expect(res.text).toContain('index.html');
587
+ expect(res.text).toContain('style.css');
588
+ expect(res.text).toContain('subdir');
589
+ } finally {
590
+ server.close();
591
+ spy.mockRestore();
592
+ }
593
+ });
594
+
595
+ test('DT_UNKNOWN Dirent has all is*() methods returning false', () => {
596
+ // Verify our test helper creates correct DT_UNKNOWN dirents
597
+ const d = createUnknownDirent('test.txt');
598
+ expect(d.isFile()).toBe(false);
599
+ expect(d.isDirectory()).toBe(false);
600
+ expect(d.isSymbolicLink()).toBe(false);
601
+ expect(d.isBlockDevice()).toBe(false);
602
+ expect(d.isCharacterDevice()).toBe(false);
603
+ expect(d.isFIFO()).toBe(false);
604
+ expect(d.isSocket()).toBe(false);
605
+
606
+ // Verify Symbol(type) is 0
607
+ const syms = Object.getOwnPropertySymbols(d);
608
+ expect(d[syms[0]]).toBe(0);
609
+ });
610
+
611
+ test('index priority is preserved with DT_UNKNOWN entries', async () => {
612
+ // When multiple index files exist, the first pattern should win
613
+ const spy = mockReaddirWithDtUnknown(tmpDir, ['index.ejs', 'index.html', 'style.css']);
614
+
615
+ const app = new Koa();
616
+ app.use(koaClassicServer(tmpDir, {
617
+ index: ['index.html', 'index.ejs'],
618
+ showDirContents: true
619
+ }));
620
+ const server = app.listen();
621
+ const request = supertest(server);
622
+
623
+ try {
624
+ const res = await request.get('/');
625
+ expect(res.status).toBe(200);
626
+ // index.html should win because it's first in the index array
627
+ expect(res.text).toContain('DT_UNKNOWN Index');
628
+ expect(res.text).not.toContain('EJS DT_UNKNOWN');
629
+ } finally {
630
+ server.close();
631
+ spy.mockRestore();
632
+ }
633
+ });
634
+ });
635
+ });
@@ -0,0 +1 @@
1
+ dot dir file
@@ -0,0 +1,2 @@
1
+ SECRET_KEY=mysecretvalue
2
+ DB_PASSWORD=hidden123
@@ -0,0 +1 @@
1
+ key data
@@ -0,0 +1 @@
1
+ secret data
@@ -0,0 +1 @@
1
+ regular
@@ -0,0 +1 @@
1
+ normal
@@ -0,0 +1 @@
1
+ NESTED_SECRET=nestedvalue
@@ -0,0 +1 @@
1
+ regular in subdir