koa-classic-server 2.3.0 → 2.5.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.
@@ -0,0 +1,550 @@
1
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2
+ //
3
+ // TEST FOR hideExtension OPTION
4
+ // This test verifies that the hideExtension option works correctly:
5
+ // - Clean URL resolution (URL without extension → file with extension)
6
+ // - Redirect from URL with extension to clean URL
7
+ // - Query string preservation
8
+ // - Conflict resolution (directory vs file, extensionless vs extension)
9
+ // - Input validation
10
+ // - Interaction with existing options (urlsReserved, useOriginalUrl, template)
11
+ //
12
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
13
+
14
+ const supertest = require('supertest');
15
+ const koaClassicServer = require('../index.cjs');
16
+ const Koa = require('koa');
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const ejs = require('ejs');
20
+
21
+ const rootDir = path.join(__dirname, 'publicWwwTest', 'hideext-test');
22
+
23
+ describe('hideExtension option tests', () => {
24
+
25
+ // ==========================================
26
+ // Clean URL Resolution
27
+ // ==========================================
28
+ describe('Clean URL resolution', () => {
29
+ let app, server, request;
30
+
31
+ beforeAll(() => {
32
+ app = new Koa();
33
+ app.use(koaClassicServer(rootDir, {
34
+ showDirContents: true,
35
+ index: ['index.ejs'],
36
+ hideExtension: { ext: '.ejs' },
37
+ template: {
38
+ ext: ['ejs'],
39
+ render: async (ctx, next, filePath) => {
40
+ const content = await fs.promises.readFile(filePath, 'utf-8');
41
+ ctx.type = 'text/html';
42
+ ctx.body = content;
43
+ }
44
+ }
45
+ }));
46
+ server = app.listen();
47
+ request = supertest(server);
48
+ });
49
+
50
+ afterAll(() => { server.close(); });
51
+
52
+ test('/about serves about.ejs', async () => {
53
+ const response = await request.get('/about');
54
+ expect(response.status).toBe(200);
55
+ expect(response.text).toContain('About Page');
56
+ });
57
+
58
+ test('/blog/articolo serves blog/articolo.ejs (multi-level path)', async () => {
59
+ const response = await request.get('/blog/articolo');
60
+ expect(response.status).toBe(200);
61
+ expect(response.text).toContain('Blog Article');
62
+ });
63
+
64
+ test('/style.css serves style.css (no interference with other extensions)', async () => {
65
+ const response = await request.get('/style.css');
66
+ expect(response.status).toBe(200);
67
+ expect(response.text).toContain('body { color: red; }');
68
+ });
69
+
70
+ test('/ serves the index file via existing index flow', async () => {
71
+ const response = await request.get('/');
72
+ expect(response.status).toBe(200);
73
+ expect(response.text).toContain('Home Page');
74
+ });
75
+ });
76
+
77
+ // ==========================================
78
+ // Redirect URL with extension → clean URL
79
+ // ==========================================
80
+ describe('Redirect URL with extension to clean URL', () => {
81
+ let app, server, request;
82
+
83
+ beforeAll(() => {
84
+ app = new Koa();
85
+ app.use(koaClassicServer(rootDir, {
86
+ showDirContents: true,
87
+ index: ['index.ejs'],
88
+ hideExtension: { ext: '.ejs' },
89
+ template: {
90
+ ext: ['ejs'],
91
+ render: async (ctx, next, filePath) => {
92
+ const content = await fs.promises.readFile(filePath, 'utf-8');
93
+ ctx.type = 'text/html';
94
+ ctx.body = content;
95
+ }
96
+ }
97
+ }));
98
+ server = app.listen();
99
+ request = supertest(server);
100
+ });
101
+
102
+ afterAll(() => { server.close(); });
103
+
104
+ test('/about.ejs → redirect 301 to /about', async () => {
105
+ const response = await request.get('/about.ejs');
106
+ expect(response.status).toBe(301);
107
+ expect(response.headers.location).toBe('/about');
108
+ });
109
+
110
+ test('/blog/articolo.ejs → redirect 301 to /blog/articolo', async () => {
111
+ const response = await request.get('/blog/articolo.ejs');
112
+ expect(response.status).toBe(301);
113
+ expect(response.headers.location).toBe('/blog/articolo');
114
+ });
115
+
116
+ test('/about.ejs?lang=it → redirect 301 to /about?lang=it (preserves query string)', async () => {
117
+ const response = await request.get('/about.ejs?lang=it');
118
+ expect(response.status).toBe(301);
119
+ expect(response.headers.location).toBe('/about?lang=it');
120
+ });
121
+
122
+ test('/index.ejs → redirect to /', async () => {
123
+ const response = await request.get('/index.ejs');
124
+ expect(response.status).toBe(301);
125
+ expect(response.headers.location).toBe('/');
126
+ });
127
+
128
+ test('/sezione/index.ejs → redirect to /sezione/', async () => {
129
+ const response = await request.get('/sezione/index.ejs');
130
+ expect(response.status).toBe(301);
131
+ expect(response.headers.location).toBe('/sezione/');
132
+ });
133
+ });
134
+
135
+ // ==========================================
136
+ // Custom redirect code (302)
137
+ // ==========================================
138
+ describe('Custom redirect code', () => {
139
+ let app, server, request;
140
+
141
+ beforeAll(() => {
142
+ app = new Koa();
143
+ app.use(koaClassicServer(rootDir, {
144
+ showDirContents: true,
145
+ index: ['index.ejs'],
146
+ hideExtension: { ext: '.ejs', redirect: 302 }
147
+ }));
148
+ server = app.listen();
149
+ request = supertest(server);
150
+ });
151
+
152
+ afterAll(() => { server.close(); });
153
+
154
+ test('/about.ejs → redirect 302 to /about', async () => {
155
+ const response = await request.get('/about.ejs');
156
+ expect(response.status).toBe(302);
157
+ expect(response.headers.location).toBe('/about');
158
+ });
159
+ });
160
+
161
+ // ==========================================
162
+ // Directory/file conflict (showDirContents: true)
163
+ // ==========================================
164
+ describe('Directory/file conflict with showDirContents: true', () => {
165
+ let app, server, request;
166
+
167
+ beforeAll(() => {
168
+ app = new Koa();
169
+ app.use(koaClassicServer(rootDir, {
170
+ showDirContents: true,
171
+ index: ['index.html'],
172
+ hideExtension: { ext: '.ejs' },
173
+ template: {
174
+ ext: ['ejs'],
175
+ render: async (ctx, next, filePath) => {
176
+ const content = await fs.promises.readFile(filePath, 'utf-8');
177
+ ctx.type = 'text/html';
178
+ ctx.body = content;
179
+ }
180
+ }
181
+ }));
182
+ server = app.listen();
183
+ request = supertest(server);
184
+ });
185
+
186
+ afterAll(() => { server.close(); });
187
+
188
+ test('/about serves about.ejs (file wins over directory)', async () => {
189
+ const response = await request.get('/about');
190
+ expect(response.status).toBe(200);
191
+ expect(response.text).toContain('About Page');
192
+ });
193
+
194
+ test('/about/ shows the directory index or directory contents', async () => {
195
+ const response = await request.get('/about/');
196
+ expect(response.status).toBe(200);
197
+ // The about/ directory has index.html, so it's served as the index file
198
+ expect(response.text).toContain('About Directory Index');
199
+ });
200
+ });
201
+
202
+ // ==========================================
203
+ // Trailing slash without directory → 404
204
+ // ==========================================
205
+ describe('Trailing slash without directory', () => {
206
+ let app, server, request;
207
+
208
+ beforeAll(() => {
209
+ app = new Koa();
210
+ app.use(koaClassicServer(rootDir, {
211
+ showDirContents: true,
212
+ hideExtension: { ext: '.ejs' }
213
+ }));
214
+ server = app.listen();
215
+ request = supertest(server);
216
+ });
217
+
218
+ afterAll(() => { server.close(); });
219
+
220
+ test('/nonexistent/ returns 404', async () => {
221
+ const response = await request.get('/nonexistent/');
222
+ expect(response.status).toBe(404);
223
+ });
224
+ });
225
+
226
+ // ==========================================
227
+ // File without extension vs .ejs conflict
228
+ // ==========================================
229
+ describe('File without extension vs .ejs conflict', () => {
230
+ let app, server, request;
231
+
232
+ beforeAll(() => {
233
+ app = new Koa();
234
+ app.use(koaClassicServer(rootDir, {
235
+ showDirContents: true,
236
+ hideExtension: { ext: '.ejs' },
237
+ template: {
238
+ ext: ['ejs'],
239
+ render: async (ctx, next, filePath) => {
240
+ const content = await fs.promises.readFile(filePath, 'utf-8');
241
+ ctx.type = 'text/html';
242
+ ctx.body = content;
243
+ }
244
+ }
245
+ }));
246
+ server = app.listen();
247
+ request = supertest(server);
248
+ });
249
+
250
+ afterAll(() => { server.close(); });
251
+
252
+ test('/conflict-test/pagina serves pagina.ejs (ejs wins over extensionless file)', async () => {
253
+ const response = await request.get('/conflict-test/pagina');
254
+ expect(response.status).toBe(200);
255
+ expect(response.text).toContain('Pagina EJS');
256
+ });
257
+ });
258
+
259
+ // ==========================================
260
+ // Interaction with urlsReserved
261
+ // ==========================================
262
+ describe('Interaction with urlsReserved', () => {
263
+ let app, server, request;
264
+
265
+ beforeAll(() => {
266
+ app = new Koa();
267
+
268
+ // Add a "next" middleware to catch reserved URLs
269
+ app.use(async (ctx, next) => {
270
+ await next();
271
+ if (ctx.status === 404 && ctx._passedToNext) {
272
+ ctx.status = 200;
273
+ ctx.body = 'RESERVED';
274
+ }
275
+ });
276
+
277
+ const middleware = koaClassicServer(rootDir, {
278
+ showDirContents: true,
279
+ index: ['index.ejs'],
280
+ urlsReserved: ['/blog'],
281
+ hideExtension: { ext: '.ejs' },
282
+ template: {
283
+ ext: ['ejs'],
284
+ render: async (ctx, next, filePath) => {
285
+ const content = await fs.promises.readFile(filePath, 'utf-8');
286
+ ctx.type = 'text/html';
287
+ ctx.body = content;
288
+ }
289
+ }
290
+ });
291
+
292
+ // Wrap middleware to track next() calls
293
+ app.use(async (ctx, next) => {
294
+ const originalNext = next;
295
+ await middleware(ctx, async () => {
296
+ ctx._passedToNext = true;
297
+ await originalNext();
298
+ });
299
+ });
300
+
301
+ server = app.listen();
302
+ request = supertest(server);
303
+ });
304
+
305
+ afterAll(() => { server.close(); });
306
+
307
+ test('/blog is reserved (passed to next middleware)', async () => {
308
+ const response = await request.get('/blog');
309
+ // blog is a directory and is reserved, so it passes to next
310
+ expect(response.text).toBe('RESERVED');
311
+ });
312
+
313
+ test('/about still resolves about.ejs normally', async () => {
314
+ const response = await request.get('/about');
315
+ expect(response.status).toBe(200);
316
+ expect(response.text).toContain('About Page');
317
+ });
318
+ });
319
+
320
+ // ==========================================
321
+ // Interaction with useOriginalUrl
322
+ // ==========================================
323
+ describe('Interaction with useOriginalUrl', () => {
324
+ let app, server, request;
325
+
326
+ beforeAll(() => {
327
+ app = new Koa();
328
+
329
+ // i18n middleware that rewrites URLs
330
+ app.use(async (ctx, next) => {
331
+ if (ctx.path.match(/^\/it\//)) {
332
+ ctx.url = ctx.path.replace(/^\/it/, '');
333
+ }
334
+ await next();
335
+ });
336
+
337
+ app.use(koaClassicServer(rootDir, {
338
+ showDirContents: true,
339
+ useOriginalUrl: false,
340
+ hideExtension: { ext: '.ejs' },
341
+ template: {
342
+ ext: ['ejs'],
343
+ render: async (ctx, next, filePath) => {
344
+ const content = await fs.promises.readFile(filePath, 'utf-8');
345
+ ctx.type = 'text/html';
346
+ ctx.body = content;
347
+ }
348
+ }
349
+ }));
350
+
351
+ server = app.listen();
352
+ request = supertest(server);
353
+ });
354
+
355
+ afterAll(() => { server.close(); });
356
+
357
+ test('redirect uses ctx.originalUrl (preserves /it/ prefix)', async () => {
358
+ const response = await request.get('/it/about.ejs');
359
+ expect(response.status).toBe(301);
360
+ // Redirect should use originalUrl: /it/about (not /about)
361
+ expect(response.headers.location).toBe('/it/about');
362
+ });
363
+
364
+ test('clean URL resolves through rewritten URL', async () => {
365
+ const response = await request.get('/it/about');
366
+ expect(response.status).toBe(200);
367
+ expect(response.text).toContain('About Page');
368
+ });
369
+ });
370
+
371
+ // ==========================================
372
+ // Case-sensitive extension matching
373
+ // ==========================================
374
+ describe('Case-sensitive extension matching', () => {
375
+ let app, server, request;
376
+
377
+ beforeAll(() => {
378
+ app = new Koa();
379
+ app.use(koaClassicServer(rootDir, {
380
+ showDirContents: true,
381
+ hideExtension: { ext: '.ejs' }
382
+ }));
383
+ server = app.listen();
384
+ request = supertest(server);
385
+ });
386
+
387
+ afterAll(() => { server.close(); });
388
+
389
+ test('/about.EJS is not handled by hideExtension (case-sensitive)', async () => {
390
+ const response = await request.get('/about.EJS');
391
+ // The file about.EJS exists, should be served normally (not redirected)
392
+ expect(response.status).toBe(200);
393
+ // Should NOT be a redirect
394
+ expect(response.status).not.toBe(301);
395
+ });
396
+ });
397
+
398
+ // ==========================================
399
+ // URLs with different extensions (no interference)
400
+ // ==========================================
401
+ describe('URLs with different extensions', () => {
402
+ let app, server, request;
403
+
404
+ beforeAll(() => {
405
+ app = new Koa();
406
+ app.use(koaClassicServer(rootDir, {
407
+ showDirContents: true,
408
+ hideExtension: { ext: '.ejs' }
409
+ }));
410
+ server = app.listen();
411
+ request = supertest(server);
412
+ });
413
+
414
+ afterAll(() => { server.close(); });
415
+
416
+ test('/file.ejs.bak is not interfered with', async () => {
417
+ const response = await request.get('/file.ejs.bak');
418
+ // .bak is the extension, not .ejs - should be served normally
419
+ expect(response.status).toBe(200);
420
+ expect(response.status).not.toBe(301);
421
+ });
422
+
423
+ test('/photo.txt is not interfered with', async () => {
424
+ const response = await request.get('/photo.txt');
425
+ expect(response.status).toBe(200);
426
+ expect(response.status).not.toBe(301);
427
+ });
428
+
429
+ test('/style.css is not interfered with', async () => {
430
+ const response = await request.get('/style.css');
431
+ expect(response.status).toBe(200);
432
+ expect(response.status).not.toBe(301);
433
+ });
434
+ });
435
+
436
+ // ==========================================
437
+ // Template engine integration
438
+ // ==========================================
439
+ describe('Template engine integration', () => {
440
+ let app, server, request;
441
+
442
+ beforeAll(() => {
443
+ app = new Koa();
444
+ app.use(koaClassicServer(rootDir, {
445
+ showDirContents: true,
446
+ index: ['index.ejs'],
447
+ hideExtension: { ext: '.ejs' },
448
+ template: {
449
+ ext: ['ejs'],
450
+ render: async (ctx, next, filePath) => {
451
+ const templateContent = await fs.promises.readFile(filePath, 'utf-8');
452
+ const html = ejs.render(templateContent, { title: 'Test Title' });
453
+ ctx.type = 'text/html';
454
+ ctx.body = html;
455
+ }
456
+ }
457
+ }));
458
+ server = app.listen();
459
+ request = supertest(server);
460
+ });
461
+
462
+ afterAll(() => { server.close(); });
463
+
464
+ test('file resolved via hideExtension passes correctly to template engine', async () => {
465
+ const response = await request.get('/about');
466
+ expect(response.status).toBe(200);
467
+ expect(response.type).toBe('text/html');
468
+ expect(response.text).toContain('About Page');
469
+ });
470
+ });
471
+
472
+ // ==========================================
473
+ // Input Validation
474
+ // ==========================================
475
+ describe('Input validation', () => {
476
+
477
+ test('hideExtension: true → throws Error', () => {
478
+ expect(() => {
479
+ koaClassicServer(rootDir, {
480
+ hideExtension: true
481
+ });
482
+ }).toThrow();
483
+ });
484
+
485
+ test('hideExtension: {} → throws Error (missing ext)', () => {
486
+ expect(() => {
487
+ koaClassicServer(rootDir, {
488
+ hideExtension: {}
489
+ });
490
+ }).toThrow();
491
+ });
492
+
493
+ test('hideExtension: { ext: "" } → throws Error (empty ext)', () => {
494
+ expect(() => {
495
+ koaClassicServer(rootDir, {
496
+ hideExtension: { ext: '' }
497
+ });
498
+ }).toThrow();
499
+ });
500
+
501
+ test('hideExtension: { ext: "ejs" } → warning + normalizes to ".ejs"', () => {
502
+ const originalWarn = console.warn;
503
+ const warnings = [];
504
+ console.warn = (...args) => { warnings.push(args); };
505
+
506
+ try {
507
+ const middleware = koaClassicServer(rootDir, {
508
+ hideExtension: { ext: 'ejs' }
509
+ });
510
+ // Should not throw
511
+ expect(middleware).toBeDefined();
512
+ // Should have issued a warning
513
+ expect(warnings.length).toBeGreaterThan(0);
514
+ expect(warnings[0][1]).toContain('hideExtension.ext should start with a dot');
515
+ } finally {
516
+ console.warn = originalWarn;
517
+ }
518
+ });
519
+
520
+ test('hideExtension: { ext: ".ejs", redirect: "abc" } → throws Error (redirect not numeric)', () => {
521
+ expect(() => {
522
+ koaClassicServer(rootDir, {
523
+ hideExtension: { ext: '.ejs', redirect: 'abc' }
524
+ });
525
+ }).toThrow();
526
+ });
527
+
528
+ test('hideExtension: { ext: ".ejs" } → valid, default redirect 301', () => {
529
+ expect(() => {
530
+ koaClassicServer(rootDir, {
531
+ hideExtension: { ext: '.ejs' }
532
+ });
533
+ }).not.toThrow();
534
+ });
535
+
536
+ test('hideExtension: { ext: ".ejs", redirect: 302 } → valid', () => {
537
+ expect(() => {
538
+ koaClassicServer(rootDir, {
539
+ hideExtension: { ext: '.ejs', redirect: 302 }
540
+ });
541
+ }).not.toThrow();
542
+ });
543
+
544
+ test('hideExtension: undefined → feature disabled (no error)', () => {
545
+ expect(() => {
546
+ koaClassicServer(rootDir, {});
547
+ }).not.toThrow();
548
+ });
549
+ });
550
+ });
@@ -0,0 +1 @@
1
+ <h1>About Directory Index</h1>
@@ -0,0 +1 @@
1
+ <h1>About UPPERCASE</h1>
@@ -0,0 +1,2 @@
1
+ <h1>About Page</h1>
2
+ <p>This is the about page rendered from about.ejs</p>
@@ -0,0 +1,2 @@
1
+ <h1>Blog Article</h1>
2
+ <p>This is a blog article rendered from blog/articolo.ejs</p>
@@ -0,0 +1 @@
1
+ plain pagina file
@@ -0,0 +1 @@
1
+ <h1>Pagina EJS</h1>
@@ -0,0 +1 @@
1
+ backup file
@@ -0,0 +1,2 @@
1
+ <h1>Home Page</h1>
2
+ <p>This is the index page</p>
@@ -0,0 +1 @@
1
+ This is a photo description file
@@ -0,0 +1 @@
1
+ <h1>Sezione Index</h1>
@@ -0,0 +1 @@
1
+ body { color: red; }