koa-classic-server 2.1.4 → 2.4.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 +110 -35
- package/__tests__/deprecation-warnings.test.js +217 -0
- package/__tests__/publicWwwTest/test-page.html +1 -0
- package/__tests__/symlink.test.js +438 -0
- package/__tests__/useOriginalUrl.test.js +213 -0
- package/docs/CHANGELOG.md +160 -0
- package/docs/DOCUMENTATION.md +53 -5
- package/index.cjs +140 -38
- package/package.json +1 -1
|
@@ -0,0 +1,438 @@
|
|
|
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
|
+
showDirContents: 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
|
+
showDirContents: 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
|
+
expect(res.text).toContain('EJS via Symlink');
|
|
165
|
+
expect(res.text).not.toContain('Index of');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// =========================================================================
|
|
170
|
+
// 3. BUG FIX - Direct GET to symlinked file returns 200
|
|
171
|
+
// =========================================================================
|
|
172
|
+
describe('direct GET to symlinked file (bug fix)', () => {
|
|
173
|
+
let server, request;
|
|
174
|
+
|
|
175
|
+
beforeAll(() => {
|
|
176
|
+
const app = new Koa();
|
|
177
|
+
app.use(koaClassicServer(tmpDir, {
|
|
178
|
+
index: [],
|
|
179
|
+
showDirContents: true
|
|
180
|
+
}));
|
|
181
|
+
server = app.listen();
|
|
182
|
+
request = supertest(server);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
afterAll(() => { server?.close(); });
|
|
186
|
+
|
|
187
|
+
test('GET /index.ejs via symlink returns 200', async () => {
|
|
188
|
+
const res = await request.get('/index.ejs');
|
|
189
|
+
expect(res.status).toBe(200);
|
|
190
|
+
expect(res.text).toContain('EJS via Symlink');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('GET /linked-style.css via symlink returns 200 with correct mime', async () => {
|
|
194
|
+
const res = await request.get('/linked-style.css');
|
|
195
|
+
expect(res.status).toBe(200);
|
|
196
|
+
expect(res.headers['content-type']).toMatch(/css/);
|
|
197
|
+
expect(res.text).toContain('body { color: red; }');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// =========================================================================
|
|
202
|
+
// 4. BUG FIX - Symlink to file with template engine
|
|
203
|
+
// =========================================================================
|
|
204
|
+
describe('EJS template via symlink (bug fix)', () => {
|
|
205
|
+
let server, request;
|
|
206
|
+
|
|
207
|
+
beforeAll(() => {
|
|
208
|
+
const app = new Koa();
|
|
209
|
+
app.use(koaClassicServer(tmpDir, {
|
|
210
|
+
index: ['index.ejs'],
|
|
211
|
+
showDirContents: true,
|
|
212
|
+
template: {
|
|
213
|
+
ext: ['ejs'],
|
|
214
|
+
render: async (ctx, next, filePath) => {
|
|
215
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
216
|
+
ctx.type = 'text/html';
|
|
217
|
+
ctx.body = content;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}));
|
|
221
|
+
server = app.listen();
|
|
222
|
+
request = supertest(server);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
afterAll(() => { server?.close(); });
|
|
226
|
+
|
|
227
|
+
test('GET / renders EJS template through symlink', async () => {
|
|
228
|
+
const res = await request.get('/');
|
|
229
|
+
expect(res.status).toBe(200);
|
|
230
|
+
expect(res.headers['content-type']).toMatch(/html/);
|
|
231
|
+
expect(res.text).toContain('EJS via Symlink');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// =========================================================================
|
|
236
|
+
// 5. Directory as symlink
|
|
237
|
+
// =========================================================================
|
|
238
|
+
describe('symlink to directory', () => {
|
|
239
|
+
let server, request;
|
|
240
|
+
|
|
241
|
+
beforeAll(() => {
|
|
242
|
+
const app = new Koa();
|
|
243
|
+
app.use(koaClassicServer(tmpDir, {
|
|
244
|
+
index: [],
|
|
245
|
+
showDirContents: true
|
|
246
|
+
}));
|
|
247
|
+
server = app.listen();
|
|
248
|
+
request = supertest(server);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
afterAll(() => { server?.close(); });
|
|
252
|
+
|
|
253
|
+
test('GET /linked-subdir/ lists directory contents', async () => {
|
|
254
|
+
const res = await request.get('/linked-subdir/');
|
|
255
|
+
expect(res.status).toBe(200);
|
|
256
|
+
expect(res.text).toContain('file.txt');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('GET /linked-subdir/file.txt serves file inside symlinked dir', async () => {
|
|
260
|
+
const res = await request.get('/linked-subdir/file.txt');
|
|
261
|
+
expect(res.status).toBe(200);
|
|
262
|
+
expect(res.text).toContain('content inside real-subdir');
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// =========================================================================
|
|
267
|
+
// 6. Broken symlink
|
|
268
|
+
// =========================================================================
|
|
269
|
+
describe('broken symlink', () => {
|
|
270
|
+
let server, request;
|
|
271
|
+
|
|
272
|
+
beforeAll(() => {
|
|
273
|
+
const app = new Koa();
|
|
274
|
+
app.use(koaClassicServer(tmpDir, {
|
|
275
|
+
index: [],
|
|
276
|
+
showDirContents: true
|
|
277
|
+
}));
|
|
278
|
+
server = app.listen();
|
|
279
|
+
request = supertest(server);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
afterAll(() => { server?.close(); });
|
|
283
|
+
|
|
284
|
+
test('GET /broken-link.html returns 404, not crash', async () => {
|
|
285
|
+
const res = await request.get('/broken-link.html');
|
|
286
|
+
expect(res.status).toBe(404);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// =========================================================================
|
|
291
|
+
// 7. Circular symlink
|
|
292
|
+
// =========================================================================
|
|
293
|
+
describe('circular symlink', () => {
|
|
294
|
+
let server, request;
|
|
295
|
+
let circularExists = false;
|
|
296
|
+
|
|
297
|
+
beforeAll(() => {
|
|
298
|
+
circularExists = fs.existsSync(path.join(tmpDir, 'circular-a'));
|
|
299
|
+
const app = new Koa();
|
|
300
|
+
app.use(koaClassicServer(tmpDir, {
|
|
301
|
+
index: [],
|
|
302
|
+
showDirContents: true
|
|
303
|
+
}));
|
|
304
|
+
server = app.listen();
|
|
305
|
+
request = supertest(server);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
afterAll(() => { server?.close(); });
|
|
309
|
+
|
|
310
|
+
test('GET /circular-a does not cause infinite loop', async () => {
|
|
311
|
+
if (!circularExists) {
|
|
312
|
+
// Skip if circular symlinks could not be created on this OS
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const res = await request.get('/circular-a');
|
|
316
|
+
// Should return an error status, not hang
|
|
317
|
+
expect([404, 500]).toContain(res.status);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// =========================================================================
|
|
322
|
+
// 8. REGRESSION - Regular non-index file unchanged
|
|
323
|
+
// =========================================================================
|
|
324
|
+
describe('regular non-index file (regression)', () => {
|
|
325
|
+
let server, request;
|
|
326
|
+
|
|
327
|
+
beforeAll(() => {
|
|
328
|
+
const app = new Koa();
|
|
329
|
+
app.use(koaClassicServer(tmpDir, {
|
|
330
|
+
index: ['index.html'],
|
|
331
|
+
showDirContents: true
|
|
332
|
+
}));
|
|
333
|
+
server = app.listen();
|
|
334
|
+
request = supertest(server);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
afterAll(() => { server?.close(); });
|
|
338
|
+
|
|
339
|
+
test('GET /style.css serves regular file correctly', async () => {
|
|
340
|
+
const res = await request.get('/style.css');
|
|
341
|
+
expect(res.status).toBe(200);
|
|
342
|
+
expect(res.headers['content-type']).toMatch(/css/);
|
|
343
|
+
expect(res.text).toContain('body { color: red; }');
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// =========================================================================
|
|
348
|
+
// 9. Directory listing shows symlink indicators
|
|
349
|
+
// =========================================================================
|
|
350
|
+
describe('directory listing symlink indicators', () => {
|
|
351
|
+
let server, request;
|
|
352
|
+
|
|
353
|
+
beforeAll(() => {
|
|
354
|
+
const app = new Koa();
|
|
355
|
+
app.use(koaClassicServer(tmpDir, {
|
|
356
|
+
index: [],
|
|
357
|
+
showDirContents: true
|
|
358
|
+
}));
|
|
359
|
+
server = app.listen();
|
|
360
|
+
request = supertest(server);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
afterAll(() => { server?.close(); });
|
|
364
|
+
|
|
365
|
+
test('symlink to file shows ( Symlink ) indicator', async () => {
|
|
366
|
+
const res = await request.get('/');
|
|
367
|
+
expect(res.status).toBe(200);
|
|
368
|
+
// index.ejs is a symlink to a file
|
|
369
|
+
expect(res.text).toContain('index.ejs');
|
|
370
|
+
expect(res.text).toMatch(/index\.ejs<\/a>\s*\( Symlink \)/);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test('symlink to directory shows ( Symlink ) indicator', async () => {
|
|
374
|
+
const res = await request.get('/');
|
|
375
|
+
expect(res.status).toBe(200);
|
|
376
|
+
expect(res.text).toContain('linked-subdir');
|
|
377
|
+
expect(res.text).toMatch(/linked-subdir<\/a>\s*\( Symlink \)/);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test('broken symlink shows ( Broken Symlink ) indicator without link', async () => {
|
|
381
|
+
const res = await request.get('/');
|
|
382
|
+
expect(res.status).toBe(200);
|
|
383
|
+
expect(res.text).toContain('broken-link.html');
|
|
384
|
+
expect(res.text).toContain('( Broken Symlink )');
|
|
385
|
+
// Broken symlink name should NOT be wrapped in <a> tag
|
|
386
|
+
expect(res.text).not.toMatch(/<a[^>]*>broken-link\.html<\/a>/);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('regular file does NOT show symlink indicator', async () => {
|
|
390
|
+
const res = await request.get('/');
|
|
391
|
+
expect(res.status).toBe(200);
|
|
392
|
+
expect(res.text).toContain('style.css');
|
|
393
|
+
// style.css (not linked-style.css) should not have any symlink indicator
|
|
394
|
+
expect(res.text).not.toMatch(/>style\.css<\/a>\s*\( Symlink \)/);
|
|
395
|
+
expect(res.text).not.toMatch(/>style\.css<\/a>\s*\( Broken Symlink \)/);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test('symlink to file shows target mime type, not "unknown"', async () => {
|
|
399
|
+
const res = await request.get('/');
|
|
400
|
+
expect(res.status).toBe(200);
|
|
401
|
+
// linked-style.css is a symlink to style.css - should show text/css mime
|
|
402
|
+
expect(res.text).toMatch(/linked-style\.css[\s\S]*?text\/css/);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test('symlink to directory shows DIR type', async () => {
|
|
406
|
+
const res = await request.get('/');
|
|
407
|
+
expect(res.status).toBe(200);
|
|
408
|
+
// linked-subdir is a symlink to a directory - should show DIR
|
|
409
|
+
expect(res.text).toMatch(/linked-subdir[\s\S]*?DIR/);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// =========================================================================
|
|
414
|
+
// 10. Symlink as index with RegExp pattern
|
|
415
|
+
// =========================================================================
|
|
416
|
+
describe('symlink as index with RegExp pattern', () => {
|
|
417
|
+
let server, request;
|
|
418
|
+
|
|
419
|
+
beforeAll(() => {
|
|
420
|
+
const app = new Koa();
|
|
421
|
+
app.use(koaClassicServer(tmpDir, {
|
|
422
|
+
index: [/index\.[eE][jJ][sS]/],
|
|
423
|
+
showDirContents: true
|
|
424
|
+
}));
|
|
425
|
+
server = app.listen();
|
|
426
|
+
request = supertest(server);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
afterAll(() => { server?.close(); });
|
|
430
|
+
|
|
431
|
+
test('GET / finds symlinked index.ejs via RegExp pattern', async () => {
|
|
432
|
+
const res = await request.get('/');
|
|
433
|
+
expect(res.status).toBe(200);
|
|
434
|
+
expect(res.text).toContain('EJS via Symlink');
|
|
435
|
+
expect(res.text).not.toContain('Index of');
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
//
|
|
3
|
+
// TEST FOR useOriginalUrl OPTION
|
|
4
|
+
// This test verifies that the useOriginalUrl option works correctly with URL rewriting middleware
|
|
5
|
+
//
|
|
6
|
+
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
7
|
+
|
|
8
|
+
const supertest = require('supertest');
|
|
9
|
+
const koaClassicServer = require('../index.cjs');
|
|
10
|
+
const Koa = require('koa');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
const rootDir = path.join(__dirname, 'publicWwwTest');
|
|
15
|
+
|
|
16
|
+
// Create a simple test file if it doesn't exist
|
|
17
|
+
const testFilePath = path.join(rootDir, 'test-page.html');
|
|
18
|
+
const testFileContent = '<!DOCTYPE html><html><head><title>Test Page</title></head><body><h1>Test Page</h1></body></html>';
|
|
19
|
+
|
|
20
|
+
beforeAll(() => {
|
|
21
|
+
// Ensure test directory exists
|
|
22
|
+
if (!fs.existsSync(rootDir)) {
|
|
23
|
+
fs.mkdirSync(rootDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Create test file
|
|
27
|
+
if (!fs.existsSync(testFilePath)) {
|
|
28
|
+
fs.writeFileSync(testFilePath, testFileContent, 'utf-8');
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('useOriginalUrl option tests', () => {
|
|
33
|
+
|
|
34
|
+
describe('Default behavior (useOriginalUrl: true)', () => {
|
|
35
|
+
let app;
|
|
36
|
+
let server;
|
|
37
|
+
|
|
38
|
+
beforeAll(() => {
|
|
39
|
+
app = new Koa();
|
|
40
|
+
|
|
41
|
+
// i18n middleware that rewrites URLs
|
|
42
|
+
app.use(async (ctx, next) => {
|
|
43
|
+
if (ctx.path.match(/^\/it\//)) {
|
|
44
|
+
// Rewrite /it/page.html to /page.html
|
|
45
|
+
ctx.url = ctx.path.replace(/^\/it/, '');
|
|
46
|
+
}
|
|
47
|
+
await next();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Serve files with default useOriginalUrl: true
|
|
51
|
+
app.use(koaClassicServer(rootDir, {
|
|
52
|
+
useOriginalUrl: true // Default behavior
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
server = app.listen();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterAll(() => {
|
|
59
|
+
server.close();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('should use ctx.originalUrl (original request path)', async () => {
|
|
63
|
+
// Request /it/test-page.html
|
|
64
|
+
// ctx.originalUrl = /it/test-page.html (unchanged)
|
|
65
|
+
// ctx.url = /test-page.html (rewritten by middleware)
|
|
66
|
+
// With useOriginalUrl: true, server looks for /it/test-page.html (which doesn't exist)
|
|
67
|
+
const response = await supertest(server).get('/it/test-page.html');
|
|
68
|
+
|
|
69
|
+
// Should return 404 because /it/test-page.html doesn't exist
|
|
70
|
+
expect(response.status).toBe(404);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('should serve file without rewriting', async () => {
|
|
74
|
+
// Request /test-page.html directly (no rewriting)
|
|
75
|
+
const response = await supertest(server).get('/test-page.html');
|
|
76
|
+
|
|
77
|
+
// Should return 200 and the file content
|
|
78
|
+
expect(response.status).toBe(200);
|
|
79
|
+
expect(response.text).toContain('Test Page');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('URL rewriting support (useOriginalUrl: false)', () => {
|
|
84
|
+
let app;
|
|
85
|
+
let server;
|
|
86
|
+
|
|
87
|
+
beforeAll(() => {
|
|
88
|
+
app = new Koa();
|
|
89
|
+
|
|
90
|
+
// i18n middleware that rewrites URLs
|
|
91
|
+
app.use(async (ctx, next) => {
|
|
92
|
+
if (ctx.path.match(/^\/it\//)) {
|
|
93
|
+
// Rewrite /it/page.html to /page.html
|
|
94
|
+
ctx.url = ctx.path.replace(/^\/it/, '');
|
|
95
|
+
}
|
|
96
|
+
await next();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Serve files with useOriginalUrl: false to use rewritten URL
|
|
100
|
+
app.use(koaClassicServer(rootDir, {
|
|
101
|
+
useOriginalUrl: false // Use ctx.url (rewritten)
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
server = app.listen();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
afterAll(() => {
|
|
108
|
+
server.close();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('should use ctx.url (rewritten path)', async () => {
|
|
112
|
+
// Request /it/test-page.html
|
|
113
|
+
// ctx.originalUrl = /it/test-page.html (unchanged)
|
|
114
|
+
// ctx.url = /test-page.html (rewritten by middleware)
|
|
115
|
+
// With useOriginalUrl: false, server looks for /test-page.html (which exists)
|
|
116
|
+
const response = await supertest(server).get('/it/test-page.html');
|
|
117
|
+
|
|
118
|
+
// Should return 200 and the file content
|
|
119
|
+
expect(response.status).toBe(200);
|
|
120
|
+
expect(response.text).toContain('Test Page');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('should still serve file without rewriting', async () => {
|
|
124
|
+
// Request /test-page.html directly (no rewriting)
|
|
125
|
+
const response = await supertest(server).get('/test-page.html');
|
|
126
|
+
|
|
127
|
+
// Should return 200 and the file content
|
|
128
|
+
expect(response.status).toBe(200);
|
|
129
|
+
expect(response.text).toContain('Test Page');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('Complex i18n routing scenario', () => {
|
|
134
|
+
let app;
|
|
135
|
+
let server;
|
|
136
|
+
|
|
137
|
+
beforeAll(() => {
|
|
138
|
+
app = new Koa();
|
|
139
|
+
|
|
140
|
+
// More complex i18n middleware
|
|
141
|
+
app.use(async (ctx, next) => {
|
|
142
|
+
const langPattern = /^\/(it|fr|de|es)\//;
|
|
143
|
+
const match = ctx.path.match(langPattern);
|
|
144
|
+
if (match) {
|
|
145
|
+
// Store language in state
|
|
146
|
+
ctx.state.lang = match[1];
|
|
147
|
+
// Strip language prefix
|
|
148
|
+
ctx.url = ctx.path.replace(langPattern, '/');
|
|
149
|
+
}
|
|
150
|
+
await next();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
app.use(koaClassicServer(rootDir, {
|
|
154
|
+
useOriginalUrl: false
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
server = app.listen();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
afterAll(() => {
|
|
161
|
+
server.close();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('should work with Italian locale (/it/)', async () => {
|
|
165
|
+
const response = await supertest(server).get('/it/test-page.html');
|
|
166
|
+
expect(response.status).toBe(200);
|
|
167
|
+
expect(response.text).toContain('Test Page');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('should work with French locale (/fr/)', async () => {
|
|
171
|
+
const response = await supertest(server).get('/fr/test-page.html');
|
|
172
|
+
expect(response.status).toBe(200);
|
|
173
|
+
expect(response.text).toContain('Test Page');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('should work with German locale (/de/)', async () => {
|
|
177
|
+
const response = await supertest(server).get('/de/test-page.html');
|
|
178
|
+
expect(response.status).toBe(200);
|
|
179
|
+
expect(response.text).toContain('Test Page');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('should work with Spanish locale (/es/)', async () => {
|
|
183
|
+
const response = await supertest(server).get('/es/test-page.html');
|
|
184
|
+
expect(response.status).toBe(200);
|
|
185
|
+
expect(response.text).toContain('Test Page');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('Backward compatibility', () => {
|
|
190
|
+
let app;
|
|
191
|
+
let server;
|
|
192
|
+
|
|
193
|
+
beforeAll(() => {
|
|
194
|
+
app = new Koa();
|
|
195
|
+
|
|
196
|
+
// No URL rewriting middleware
|
|
197
|
+
// Default useOriginalUrl (should be true)
|
|
198
|
+
app.use(koaClassicServer(rootDir));
|
|
199
|
+
|
|
200
|
+
server = app.listen();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
afterAll(() => {
|
|
204
|
+
server.close();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('should work with default options (backward compatible)', async () => {
|
|
208
|
+
const response = await supertest(server).get('/test-page.html');
|
|
209
|
+
expect(response.status).toBe(200);
|
|
210
|
+
expect(response.text).toContain('Test Page');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|