koa-classic-server 3.0.0 → 3.1.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 (105) hide show
  1. package/README.md +44 -120
  2. package/index.cjs +236 -21
  3. package/package.json +10 -5
  4. package/.github/workflows/npm-publish.yml +0 -98
  5. package/CLAUDE.md +0 -101
  6. package/__tests__/benchmark-results-baseline-v1.2.0.txt +0 -354
  7. package/__tests__/benchmark-results-optimized-v2.0.0.txt +0 -354
  8. package/__tests__/benchmark-results-v3.0.0.txt +0 -372
  9. package/__tests__/benchmark.js +0 -239
  10. package/__tests__/caching-headers.test.js +0 -556
  11. package/__tests__/compression-fixtures/data.json +0 -1
  12. package/__tests__/compression-fixtures/large.txt +0 -1
  13. package/__tests__/compression-fixtures/small.txt +0 -1
  14. package/__tests__/compression.test.js +0 -284
  15. package/__tests__/customTest/README.md +0 -6
  16. package/__tests__/customTest/loadConfig.util.js +0 -41
  17. package/__tests__/customTest/serversToLoad.util.js +0 -93
  18. package/__tests__/demo-regex-index.js +0 -140
  19. package/__tests__/deprecation-warnings.test.js +0 -105
  20. package/__tests__/directory-sorting-links.test.js +0 -135
  21. package/__tests__/dt-unknown.test.js +0 -635
  22. package/__tests__/ejs.test.js +0 -299
  23. package/__tests__/hidden-fixtures/.dot-dir/inside.txt +0 -1
  24. package/__tests__/hidden-fixtures/.env +0 -2
  25. package/__tests__/hidden-fixtures/.well-known/acme-challenge.txt +0 -1
  26. package/__tests__/hidden-fixtures/config/secrets/password.txt +0 -1
  27. package/__tests__/hidden-fixtures/data.key +0 -1
  28. package/__tests__/hidden-fixtures/file.secret +0 -1
  29. package/__tests__/hidden-fixtures/index.html +0 -1
  30. package/__tests__/hidden-fixtures/normal.txt +0 -1
  31. package/__tests__/hidden-fixtures/subdir/.env +0 -1
  32. package/__tests__/hidden-fixtures/subdir/regular.txt +0 -1
  33. package/__tests__/hidden-option.test.js +0 -407
  34. package/__tests__/hideExtension.test.js +0 -607
  35. package/__tests__/index-option.test.js +0 -449
  36. package/__tests__/index.test.js +0 -345
  37. package/__tests__/listing.test.js +0 -437
  38. package/__tests__/logger.test.js +0 -232
  39. package/__tests__/performance.test.js +0 -370
  40. package/__tests__/publicWwwTest/cartella/sottocartella/ciao.html +0 -11
  41. package/__tests__/publicWwwTest/cartella/sottocartella/provaEjs/testEjs.ejs +0 -11
  42. package/__tests__/publicWwwTest/cartella vuota con spazi nel nome/file con spazio nel nome .txt +0 -1
  43. package/__tests__/publicWwwTest/cartella vuota con spazi nel nome /file con spazio nel nome .txt +0 -1
  44. package/__tests__/publicWwwTest/ejs-templates/complex.ejs +0 -56
  45. package/__tests__/publicWwwTest/ejs-templates/index.ejs +0 -30
  46. package/__tests__/publicWwwTest/ejs-templates/simple.ejs +0 -13
  47. package/__tests__/publicWwwTest/ejs-templates/with-conditional.ejs +0 -28
  48. package/__tests__/publicWwwTest/ejs-templates/with-escaping.ejs +0 -26
  49. package/__tests__/publicWwwTest/ejs-templates/with-loop.ejs +0 -16
  50. package/__tests__/publicWwwTest/hideext-test/about/index.html +0 -1
  51. package/__tests__/publicWwwTest/hideext-test/about.EJS +0 -1
  52. package/__tests__/publicWwwTest/hideext-test/about.ejs +0 -2
  53. package/__tests__/publicWwwTest/hideext-test/blog/articolo.ejs +0 -2
  54. package/__tests__/publicWwwTest/hideext-test/conflict-test/pagina +0 -1
  55. package/__tests__/publicWwwTest/hideext-test/conflict-test/pagina.ejs +0 -1
  56. package/__tests__/publicWwwTest/hideext-test/file.ejs.bak +0 -1
  57. package/__tests__/publicWwwTest/hideext-test/index.ejs +0 -2
  58. package/__tests__/publicWwwTest/hideext-test/photo.txt +0 -1
  59. package/__tests__/publicWwwTest/hideext-test/sezione/index.ejs +0 -1
  60. package/__tests__/publicWwwTest/hideext-test/style.css +0 -1
  61. package/__tests__/publicWwwTest/ile_vuoto.txt +0 -0
  62. package/__tests__/publicWwwTest/index.html +0 -11
  63. package/__tests__/publicWwwTest/prova file .txt +0 -2
  64. package/__tests__/publicWwwTest/semplicetxt.txt +0 -1
  65. package/__tests__/publicWwwTest/test-page.html +0 -1
  66. package/__tests__/publicWwwTest/test.txt +0 -1
  67. package/__tests__/range-fixtures/sample.txt +0 -1
  68. package/__tests__/range.test.js +0 -223
  69. package/__tests__/security-headers.test.js +0 -165
  70. package/__tests__/security.test.js +0 -322
  71. package/__tests__/server-cache-fixtures/large.txt +0 -1
  72. package/__tests__/server-cache-fixtures/small.txt +0 -1
  73. package/__tests__/server-cache.test.js +0 -594
  74. package/__tests__/setup-benchmark.js +0 -178
  75. package/__tests__/symlink.test.js +0 -441
  76. package/__tests__/template-timeout.test.js +0 -321
  77. package/__tests__/test-regex-quick.js +0 -158
  78. package/__tests__/useOriginalUrl.test.js +0 -213
  79. package/docs/ACTION_PLAN.md +0 -293
  80. package/docs/BENCHMARKS.md +0 -317
  81. package/docs/CHANGELOG.md +0 -880
  82. package/docs/CODE_REVIEW.md +0 -300
  83. package/docs/DEBUG_REPORT.md +0 -593
  84. package/docs/DOCUMENTATION.md +0 -1864
  85. package/docs/EXAMPLES_INDEX_OPTION.md +0 -395
  86. package/docs/FLOW_DIAGRAM.md +0 -954
  87. package/docs/INDEX_OPTION_PRIORITY.md +0 -527
  88. package/docs/OPTIMIZATION_HTTP_CACHING.md +0 -689
  89. package/docs/OPTIMIZATION_ROADMAP_for_V3.md +0 -864
  90. package/docs/PERFORMANCE_ANALYSIS.md +0 -839
  91. package/docs/PERFORMANCE_COMPARISON.md +0 -388
  92. package/docs/noteExports.md +0 -148
  93. package/docs/security_improvement_for_V3.md +0 -421
  94. package/docs/template-engine/TEMPLATE_ENGINE_GUIDE.md +0 -1734
  95. package/docs/template-engine/esempi-incrementali.js +0 -192
  96. package/docs/template-engine/examples/esempio1-nessun-dato.ejs +0 -12
  97. package/docs/template-engine/examples/esempio2-una-variabile.ejs +0 -11
  98. package/docs/template-engine/examples/esempio3-piu-variabili.ejs +0 -15
  99. package/docs/template-engine/examples/esempio4-condizionale.ejs +0 -18
  100. package/docs/template-engine/examples/esempio5-loop.ejs +0 -18
  101. package/docs/template-engine/examples/index-esempi.html +0 -181
  102. package/docs/template-engine/examples/index.html +0 -40
  103. package/docs/template-engine/examples/test.ejs +0 -64
  104. package/eslint.config.mjs +0 -17
  105. package/jest.config.js +0 -18
@@ -1,178 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Setup script for performance benchmarks
5
- * Creates test files and directories for realistic performance testing
6
- */
7
-
8
- const fs = require('fs');
9
- const path = require('path');
10
-
11
- const BENCHMARK_DIR = path.join(__dirname, 'benchmark-data');
12
-
13
- console.log('🔧 Setting up benchmark environment...\n');
14
-
15
- // Clean up old benchmark data
16
- if (fs.existsSync(BENCHMARK_DIR)) {
17
- console.log('Cleaning old benchmark data...');
18
- fs.rmSync(BENCHMARK_DIR, { recursive: true, force: true });
19
- }
20
-
21
- // Create benchmark directory
22
- fs.mkdirSync(BENCHMARK_DIR, { recursive: true });
23
-
24
- // Create small files (1KB each)
25
- console.log('Creating small files (1KB each)...');
26
- const smallDir = path.join(BENCHMARK_DIR, 'small-files');
27
- fs.mkdirSync(smallDir);
28
- for (let i = 1; i <= 100; i++) {
29
- const content = 'X'.repeat(1024); // 1KB
30
- fs.writeFileSync(path.join(smallDir, `file-${i}.txt`), content);
31
- }
32
- console.log(`✓ Created 100 small files (1KB each) = 100KB total`);
33
-
34
- // Create medium files (100KB each)
35
- console.log('Creating medium files (100KB each)...');
36
- const mediumDir = path.join(BENCHMARK_DIR, 'medium-files');
37
- fs.mkdirSync(mediumDir);
38
- for (let i = 1; i <= 50; i++) {
39
- const content = 'X'.repeat(100 * 1024); // 100KB
40
- fs.writeFileSync(path.join(mediumDir, `file-${i}.txt`), content);
41
- }
42
- console.log(`✓ Created 50 medium files (100KB each) = 5MB total`);
43
-
44
- // Create large files (1MB each)
45
- console.log('Creating large files (1MB each)...');
46
- const largeDir = path.join(BENCHMARK_DIR, 'large-files');
47
- fs.mkdirSync(largeDir);
48
- for (let i = 1; i <= 10; i++) {
49
- const content = 'X'.repeat(1024 * 1024); // 1MB
50
- fs.writeFileSync(path.join(largeDir, `file-${i}.txt`), content);
51
- }
52
- console.log(`✓ Created 10 large files (1MB each) = 10MB total`);
53
-
54
- // Create directory with many files for listing test
55
- console.log('Creating large directory (1000 files)...');
56
- const largeDirListing = path.join(BENCHMARK_DIR, 'large-directory');
57
- fs.mkdirSync(largeDirListing);
58
- for (let i = 1; i <= 1000; i++) {
59
- const content = `File number ${i}\n`;
60
- fs.writeFileSync(path.join(largeDirListing, `item-${String(i).padStart(4, '0')}.txt`), content);
61
- }
62
- console.log(`✓ Created directory with 1000 files`);
63
-
64
- // Create directory with very many files (10,000) for stress test
65
- console.log('Creating very large directory (10,000 files) - this may take a while...');
66
- const veryLargeDirListing = path.join(BENCHMARK_DIR, 'very-large-directory');
67
- fs.mkdirSync(veryLargeDirListing);
68
- for (let i = 1; i <= 10000; i++) {
69
- const content = `File number ${i}\n`;
70
- fs.writeFileSync(path.join(veryLargeDirListing, `item-${String(i).padStart(5, '0')}.txt`), content);
71
- if (i % 1000 === 0) {
72
- process.stdout.write(` Progress: ${i}/10000 files created...\r`);
73
- }
74
- }
75
- console.log(`✓ Created directory with 10,000 files `);
76
-
77
- // Create HTML file for caching test
78
- console.log('Creating HTML files for caching test...');
79
- const htmlContent = `<!DOCTYPE html>
80
- <html>
81
- <head>
82
- <meta charset="UTF-8">
83
- <title>Benchmark Test Page</title>
84
- <style>
85
- body { font-family: Arial, sans-serif; margin: 40px; }
86
- h1 { color: #333; }
87
- p { line-height: 1.6; }
88
- </style>
89
- </head>
90
- <body>
91
- <h1>Benchmark Test Page</h1>
92
- <p>${'Lorem ipsum dolor sit amet. '.repeat(100)}</p>
93
- </body>
94
- </html>`;
95
- fs.writeFileSync(path.join(BENCHMARK_DIR, 'test.html'), htmlContent);
96
- console.log(`✓ Created HTML file for caching test`);
97
-
98
- // Create CSS file
99
- const cssContent = `
100
- body {
101
- margin: 0;
102
- padding: 20px;
103
- font-family: system-ui, -apple-system, sans-serif;
104
- }
105
- ${'h1 { color: #333; }\n'.repeat(50)}
106
- `.trim();
107
- fs.writeFileSync(path.join(BENCHMARK_DIR, 'style.css'), cssContent);
108
- console.log(`✓ Created CSS file`);
109
-
110
- // Create JS file
111
- const jsContent = `
112
- function benchmark() {
113
- console.log('Benchmark test');
114
- ${'console.log("test");\n'.repeat(100)}
115
- }
116
- `.trim();
117
- fs.writeFileSync(path.join(BENCHMARK_DIR, 'script.js'), jsContent);
118
- console.log(`✓ Created JS file`);
119
-
120
- // Create nested directory structure
121
- console.log('Creating nested directory structure...');
122
- const nestedBase = path.join(BENCHMARK_DIR, 'nested');
123
- fs.mkdirSync(nestedBase);
124
- for (let depth = 1; depth <= 5; depth++) {
125
- const dirPath = path.join(nestedBase, ...Array(depth).fill('level'));
126
- fs.mkdirSync(dirPath, { recursive: true });
127
- for (let i = 1; i <= 10; i++) {
128
- fs.writeFileSync(
129
- path.join(dirPath, `file-depth${depth}-${i}.txt`),
130
- `Depth ${depth}, File ${i}\n`
131
- );
132
- }
133
- }
134
- console.log(`✓ Created nested directory structure (5 levels deep)`);
135
-
136
- // Create .gitignore for benchmark-data
137
- const gitignorePath = path.join(BENCHMARK_DIR, '.gitignore');
138
- fs.writeFileSync(gitignorePath, '*\n');
139
- console.log(`✓ Created .gitignore to exclude benchmark data from git`);
140
-
141
- // Summary
142
- console.log('\n✅ Benchmark environment setup complete!\n');
143
- console.log('Directory structure:');
144
- console.log(' benchmark-data/');
145
- console.log(' ├── small-files/ (100 files × 1KB = 100KB)');
146
- console.log(' ├── medium-files/ (50 files × 100KB = 5MB)');
147
- console.log(' ├── large-files/ (10 files × 1MB = 10MB)');
148
- console.log(' ├── large-directory/ (1,000 files for listing test)');
149
- console.log(' ├── very-large-directory/ (10,000 files for stress test)');
150
- console.log(' ├── nested/ (5 levels deep, 10 files each)');
151
- console.log(' ├── test.html (HTML for caching test)');
152
- console.log(' ├── style.css (CSS file)');
153
- console.log(' └── script.js (JS file)');
154
-
155
- const stats = getDirSize(BENCHMARK_DIR);
156
- console.log(`\nTotal size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
157
- console.log(`Total files: ${stats.files}`);
158
-
159
- function getDirSize(dir) {
160
- let totalSize = 0;
161
- let totalFiles = 0;
162
-
163
- function traverse(currentPath) {
164
- const items = fs.readdirSync(currentPath, { withFileTypes: true });
165
- for (const item of items) {
166
- const fullPath = path.join(currentPath, item.name);
167
- if (item.isDirectory()) {
168
- traverse(fullPath);
169
- } else {
170
- totalSize += fs.statSync(fullPath).size;
171
- totalFiles++;
172
- }
173
- }
174
- }
175
-
176
- traverse(dir);
177
- return { size: totalSize, files: totalFiles };
178
- }
@@ -1,441 +0,0 @@
1
- /**
2
- * Symlink support tests for koa-classic-server
3
- *
4
- * Context: On NixOS with buildFHSEnv (chroot-like environment used for Playwright tests),
5
- * files in the www/ directory appear as symlinks to the Nix store instead of regular files.
6
- * This caused two failures:
7
- * - GET / returned a directory listing ("Index of /") instead of rendering index.ejs
8
- * - GET /index.ejs returned 404 instead of 200
9
- *
10
- * Root cause: fs.readdir({ withFileTypes: true }) classifies symlinks as
11
- * isSymbolicLink()=true, isFile()=false. The findIndexFile() function filtered
12
- * with dirent.isFile(), excluding all symlinks from index file discovery.
13
- *
14
- * The fix introduces isFileOrSymlinkToFile() / isDirOrSymlinkToDir() helpers
15
- * that follow symlinks via fs.promises.stat() only when dirent.isSymbolicLink()
16
- * is true, adding zero overhead for regular files.
17
- *
18
- * These tests also cover: Docker bind mounts, npm link, Capistrano-style deploys,
19
- * and any other scenario where files are served through symbolic links.
20
- */
21
-
22
- const fs = require('fs');
23
- const os = require('os');
24
- const path = require('path');
25
- const Koa = require('koa');
26
- const supertest = require('supertest');
27
- const koaClassicServer = require('../index.cjs');
28
-
29
- // Detect if the OS supports symlinks (Windows without dev mode may not)
30
- let symlinkSupported = true;
31
- try {
32
- const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'symlink-check-'));
33
- const testFile = path.join(testDir, 'target');
34
- const testLink = path.join(testDir, 'link');
35
- fs.writeFileSync(testFile, 'test');
36
- fs.symlinkSync(testFile, testLink);
37
- fs.rmSync(testDir, { recursive: true, force: true });
38
- } catch {
39
- symlinkSupported = false;
40
- }
41
-
42
- const describeIfSymlinks = symlinkSupported ? describe : describe.skip;
43
-
44
- describeIfSymlinks('koa-classic-server - symlink support', () => {
45
- let tmpDir;
46
-
47
- beforeAll(() => {
48
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kcs-symlink-test-'));
49
-
50
- // --- Regular files ---
51
- fs.writeFileSync(
52
- path.join(tmpDir, 'index.html'),
53
- '<html><head><title>Regular Index</title></head><body>Hello</body></html>'
54
- );
55
- fs.writeFileSync(
56
- path.join(tmpDir, 'style.css'),
57
- 'body { color: red; }'
58
- );
59
-
60
- // --- Real file that will be the symlink target for index.ejs ---
61
- fs.writeFileSync(
62
- path.join(tmpDir, 'real-index.ejs'),
63
- '<html><head><title>EJS via Symlink</title></head><body><h1>Works</h1></body></html>'
64
- );
65
-
66
- // --- Symlink to file (the core bug scenario) ---
67
- fs.symlinkSync(
68
- path.join(tmpDir, 'real-index.ejs'),
69
- path.join(tmpDir, 'index.ejs')
70
- );
71
-
72
- // --- Symlink to regular file (non-index) ---
73
- fs.symlinkSync(
74
- path.join(tmpDir, 'style.css'),
75
- path.join(tmpDir, 'linked-style.css')
76
- );
77
-
78
- // --- Real subdirectory with a file ---
79
- const realSubdir = path.join(tmpDir, 'real-subdir');
80
- fs.mkdirSync(realSubdir);
81
- fs.writeFileSync(
82
- path.join(realSubdir, 'file.txt'),
83
- 'content inside real-subdir'
84
- );
85
-
86
- // --- Symlink to directory ---
87
- fs.symlinkSync(
88
- realSubdir,
89
- path.join(tmpDir, 'linked-subdir')
90
- );
91
-
92
- // --- Broken symlink (target does not exist) ---
93
- fs.symlinkSync(
94
- path.join(tmpDir, 'non-existent-file.html'),
95
- path.join(tmpDir, 'broken-link.html')
96
- );
97
-
98
- // --- Circular symlinks ---
99
- try {
100
- fs.symlinkSync(
101
- path.join(tmpDir, 'circular-b'),
102
- path.join(tmpDir, 'circular-a')
103
- );
104
- fs.symlinkSync(
105
- path.join(tmpDir, 'circular-a'),
106
- path.join(tmpDir, 'circular-b')
107
- );
108
- } catch {
109
- // Some systems may not support circular symlinks creation
110
- }
111
- });
112
-
113
- afterAll(() => {
114
- fs.rmSync(tmpDir, { recursive: true, force: true });
115
- });
116
-
117
- // =========================================================================
118
- // 1. REGRESSION - Regular file as index
119
- // =========================================================================
120
- describe('regular file as index (regression)', () => {
121
- let server, request;
122
-
123
- beforeAll(() => {
124
- const app = new Koa();
125
- app.use(koaClassicServer(tmpDir, {
126
- index: ['index.html'],
127
- dirListing: { enabled: true }
128
- }));
129
- server = app.listen();
130
- request = supertest(server);
131
- });
132
-
133
- afterAll(() => { server?.close(); });
134
-
135
- test('GET / serves regular index.html, not directory listing', async () => {
136
- const res = await request.get('/');
137
- expect(res.status).toBe(200);
138
- expect(res.text).toContain('Regular Index');
139
- expect(res.text).not.toContain('Index of');
140
- });
141
- });
142
-
143
- // =========================================================================
144
- // 2. BUG FIX - Symlink to file as index
145
- // =========================================================================
146
- describe('symlink to file as index (bug fix)', () => {
147
- let server, request;
148
-
149
- beforeAll(() => {
150
- const app = new Koa();
151
- app.use(koaClassicServer(tmpDir, {
152
- index: ['index.ejs'],
153
- dirListing: { enabled: true }
154
- }));
155
- server = app.listen();
156
- request = supertest(server);
157
- });
158
-
159
- afterAll(() => { server?.close(); });
160
-
161
- test('GET / serves symlinked index.ejs, not directory listing', async () => {
162
- const res = await request.get('/');
163
- expect(res.status).toBe(200);
164
- const body = res.text !== undefined ? res.text : res.body.toString('utf8');
165
- expect(body).toContain('EJS via Symlink');
166
- expect(body).not.toContain('Index of');
167
- });
168
- });
169
-
170
- // =========================================================================
171
- // 3. BUG FIX - Direct GET to symlinked file returns 200
172
- // =========================================================================
173
- describe('direct GET to symlinked file (bug fix)', () => {
174
- let server, request;
175
-
176
- beforeAll(() => {
177
- const app = new Koa();
178
- app.use(koaClassicServer(tmpDir, {
179
- index: [],
180
- dirListing: { enabled: true }
181
- }));
182
- server = app.listen();
183
- request = supertest(server);
184
- });
185
-
186
- afterAll(() => { server?.close(); });
187
-
188
- test('GET /index.ejs via symlink returns 200', async () => {
189
- const res = await request.get('/index.ejs');
190
- expect(res.status).toBe(200);
191
- const body = res.text !== undefined ? res.text : res.body.toString('utf8');
192
- expect(body).toContain('EJS via Symlink');
193
- });
194
-
195
- test('GET /linked-style.css via symlink returns 200 with correct mime', async () => {
196
- const res = await request.get('/linked-style.css');
197
- expect(res.status).toBe(200);
198
- expect(res.headers['content-type']).toMatch(/css/);
199
- expect(res.text).toContain('body { color: red; }');
200
- });
201
- });
202
-
203
- // =========================================================================
204
- // 4. BUG FIX - Symlink to file with template engine
205
- // =========================================================================
206
- describe('EJS template via symlink (bug fix)', () => {
207
- let server, request;
208
-
209
- beforeAll(() => {
210
- const app = new Koa();
211
- app.use(koaClassicServer(tmpDir, {
212
- index: ['index.ejs'],
213
- dirListing: { enabled: true },
214
- template: {
215
- ext: ['ejs'],
216
- render: async (ctx, next, filePath) => {
217
- const content = await fs.promises.readFile(filePath, 'utf-8');
218
- ctx.type = 'text/html';
219
- ctx.body = content;
220
- }
221
- }
222
- }));
223
- server = app.listen();
224
- request = supertest(server);
225
- });
226
-
227
- afterAll(() => { server?.close(); });
228
-
229
- test('GET / renders EJS template through symlink', async () => {
230
- const res = await request.get('/');
231
- expect(res.status).toBe(200);
232
- expect(res.headers['content-type']).toMatch(/html/);
233
- expect(res.text).toContain('EJS via Symlink');
234
- });
235
- });
236
-
237
- // =========================================================================
238
- // 5. Directory as symlink
239
- // =========================================================================
240
- describe('symlink to directory', () => {
241
- let server, request;
242
-
243
- beforeAll(() => {
244
- const app = new Koa();
245
- app.use(koaClassicServer(tmpDir, {
246
- index: [],
247
- dirListing: { enabled: true }
248
- }));
249
- server = app.listen();
250
- request = supertest(server);
251
- });
252
-
253
- afterAll(() => { server?.close(); });
254
-
255
- test('GET /linked-subdir/ lists directory contents', async () => {
256
- const res = await request.get('/linked-subdir/');
257
- expect(res.status).toBe(200);
258
- expect(res.text).toContain('file.txt');
259
- });
260
-
261
- test('GET /linked-subdir/file.txt serves file inside symlinked dir', async () => {
262
- const res = await request.get('/linked-subdir/file.txt');
263
- expect(res.status).toBe(200);
264
- expect(res.text).toContain('content inside real-subdir');
265
- });
266
- });
267
-
268
- // =========================================================================
269
- // 6. Broken symlink
270
- // =========================================================================
271
- describe('broken symlink', () => {
272
- let server, request;
273
-
274
- beforeAll(() => {
275
- const app = new Koa();
276
- app.use(koaClassicServer(tmpDir, {
277
- index: [],
278
- dirListing: { enabled: true }
279
- }));
280
- server = app.listen();
281
- request = supertest(server);
282
- });
283
-
284
- afterAll(() => { server?.close(); });
285
-
286
- test('GET /broken-link.html returns 404, not crash', async () => {
287
- const res = await request.get('/broken-link.html');
288
- expect(res.status).toBe(404);
289
- });
290
- });
291
-
292
- // =========================================================================
293
- // 7. Circular symlink
294
- // =========================================================================
295
- describe('circular symlink', () => {
296
- let server, request;
297
- let circularExists = false;
298
-
299
- beforeAll(() => {
300
- circularExists = fs.existsSync(path.join(tmpDir, 'circular-a'));
301
- const app = new Koa();
302
- app.use(koaClassicServer(tmpDir, {
303
- index: [],
304
- dirListing: { enabled: true }
305
- }));
306
- server = app.listen();
307
- request = supertest(server);
308
- });
309
-
310
- afterAll(() => { server?.close(); });
311
-
312
- test('GET /circular-a does not cause infinite loop', async () => {
313
- if (!circularExists) {
314
- // Skip if circular symlinks could not be created on this OS
315
- return;
316
- }
317
- const res = await request.get('/circular-a');
318
- // Should return an error status, not hang
319
- expect([404, 500]).toContain(res.status);
320
- });
321
- });
322
-
323
- // =========================================================================
324
- // 8. REGRESSION - Regular non-index file unchanged
325
- // =========================================================================
326
- describe('regular non-index file (regression)', () => {
327
- let server, request;
328
-
329
- beforeAll(() => {
330
- const app = new Koa();
331
- app.use(koaClassicServer(tmpDir, {
332
- index: ['index.html'],
333
- dirListing: { enabled: true }
334
- }));
335
- server = app.listen();
336
- request = supertest(server);
337
- });
338
-
339
- afterAll(() => { server?.close(); });
340
-
341
- test('GET /style.css serves regular file correctly', async () => {
342
- const res = await request.get('/style.css');
343
- expect(res.status).toBe(200);
344
- expect(res.headers['content-type']).toMatch(/css/);
345
- expect(res.text).toContain('body { color: red; }');
346
- });
347
- });
348
-
349
- // =========================================================================
350
- // 9. Directory listing shows symlink indicators
351
- // =========================================================================
352
- describe('directory listing symlink indicators', () => {
353
- let server, request;
354
-
355
- beforeAll(() => {
356
- const app = new Koa();
357
- app.use(koaClassicServer(tmpDir, {
358
- index: [],
359
- dirListing: { enabled: true }
360
- }));
361
- server = app.listen();
362
- request = supertest(server);
363
- });
364
-
365
- afterAll(() => { server?.close(); });
366
-
367
- test('symlink to file shows ( Symlink ) indicator', async () => {
368
- const res = await request.get('/');
369
- expect(res.status).toBe(200);
370
- // index.ejs is a symlink to a file
371
- expect(res.text).toContain('index.ejs');
372
- expect(res.text).toMatch(/index\.ejs<\/a>\s*\( Symlink \)/);
373
- });
374
-
375
- test('symlink to directory shows ( Symlink ) indicator', async () => {
376
- const res = await request.get('/');
377
- expect(res.status).toBe(200);
378
- expect(res.text).toContain('linked-subdir');
379
- expect(res.text).toMatch(/linked-subdir<\/a>\s*\( Symlink \)/);
380
- });
381
-
382
- test('broken symlink shows ( Broken Symlink ) indicator without link', async () => {
383
- const res = await request.get('/');
384
- expect(res.status).toBe(200);
385
- expect(res.text).toContain('broken-link.html');
386
- expect(res.text).toContain('( Broken Symlink )');
387
- // Broken symlink name should NOT be wrapped in <a> tag
388
- expect(res.text).not.toMatch(/<a[^>]*>broken-link\.html<\/a>/);
389
- });
390
-
391
- test('regular file does NOT show symlink indicator', async () => {
392
- const res = await request.get('/');
393
- expect(res.status).toBe(200);
394
- expect(res.text).toContain('style.css');
395
- // style.css (not linked-style.css) should not have any symlink indicator
396
- expect(res.text).not.toMatch(/>style\.css<\/a>\s*\( Symlink \)/);
397
- expect(res.text).not.toMatch(/>style\.css<\/a>\s*\( Broken Symlink \)/);
398
- });
399
-
400
- test('symlink to file shows target mime type, not "unknown"', async () => {
401
- const res = await request.get('/');
402
- expect(res.status).toBe(200);
403
- // linked-style.css is a symlink to style.css - should show text/css mime
404
- expect(res.text).toMatch(/linked-style\.css[\s\S]*?text\/css/);
405
- });
406
-
407
- test('symlink to directory shows DIR type', async () => {
408
- const res = await request.get('/');
409
- expect(res.status).toBe(200);
410
- // linked-subdir is a symlink to a directory - should show DIR
411
- expect(res.text).toMatch(/linked-subdir[\s\S]*?DIR/);
412
- });
413
- });
414
-
415
- // =========================================================================
416
- // 10. Symlink as index with RegExp pattern
417
- // =========================================================================
418
- describe('symlink as index with RegExp pattern', () => {
419
- let server, request;
420
-
421
- beforeAll(() => {
422
- const app = new Koa();
423
- app.use(koaClassicServer(tmpDir, {
424
- index: [/index\.[eE][jJ][sS]/],
425
- dirListing: { enabled: true }
426
- }));
427
- server = app.listen();
428
- request = supertest(server);
429
- });
430
-
431
- afterAll(() => { server?.close(); });
432
-
433
- test('GET / finds symlinked index.ejs via RegExp pattern', async () => {
434
- const res = await request.get('/');
435
- expect(res.status).toBe(200);
436
- const body = res.text !== undefined ? res.text : res.body.toString('utf8');
437
- expect(body).toContain('EJS via Symlink');
438
- expect(body).not.toContain('Index of');
439
- });
440
- });
441
- });