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,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 @@
|
|
|
1
|
+
acme
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
password
|
|
@@ -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
|