koa-classic-server 3.0.1 → 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.
- package/README.md +44 -120
- package/index.cjs +200 -21
- package/package.json +10 -5
- package/.github/workflows/npm-publish.yml +0 -98
- package/CLAUDE.md +0 -101
- package/__tests__/benchmark-results-baseline-v1.2.0.txt +0 -354
- package/__tests__/benchmark-results-optimized-v2.0.0.txt +0 -354
- package/__tests__/benchmark-results-v3.0.0.txt +0 -372
- package/__tests__/benchmark.js +0 -239
- package/__tests__/caching-headers.test.js +0 -556
- package/__tests__/compression-fixtures/data.json +0 -1
- package/__tests__/compression-fixtures/large.txt +0 -1
- package/__tests__/compression-fixtures/small.txt +0 -1
- package/__tests__/compression.test.js +0 -284
- package/__tests__/customTest/README.md +0 -6
- package/__tests__/customTest/loadConfig.util.js +0 -41
- package/__tests__/customTest/serversToLoad.util.js +0 -93
- package/__tests__/demo-regex-index.js +0 -140
- package/__tests__/deprecation-warnings.test.js +0 -105
- package/__tests__/directory-sorting-links.test.js +0 -135
- package/__tests__/dt-unknown.test.js +0 -635
- package/__tests__/ejs.test.js +0 -299
- package/__tests__/head-method.test.js +0 -160
- package/__tests__/hidden-fixtures/.dot-dir/inside.txt +0 -1
- package/__tests__/hidden-fixtures/.env +0 -2
- package/__tests__/hidden-fixtures/.well-known/acme-challenge.txt +0 -1
- package/__tests__/hidden-fixtures/config/secrets/password.txt +0 -1
- package/__tests__/hidden-fixtures/data.key +0 -1
- package/__tests__/hidden-fixtures/file.secret +0 -1
- package/__tests__/hidden-fixtures/index.html +0 -1
- package/__tests__/hidden-fixtures/normal.txt +0 -1
- package/__tests__/hidden-fixtures/subdir/.env +0 -1
- package/__tests__/hidden-fixtures/subdir/regular.txt +0 -1
- package/__tests__/hidden-option.test.js +0 -407
- package/__tests__/hideExtension.test.js +0 -607
- package/__tests__/index-option.test.js +0 -449
- package/__tests__/index.test.js +0 -345
- package/__tests__/listing.test.js +0 -437
- package/__tests__/logger.test.js +0 -232
- package/__tests__/performance.test.js +0 -370
- package/__tests__/publicWwwTest/cartella/sottocartella/ciao.html +0 -11
- package/__tests__/publicWwwTest/cartella/sottocartella/provaEjs/testEjs.ejs +0 -11
- package/__tests__/publicWwwTest/cartella vuota con spazi nel nome/file con spazio nel nome .txt +0 -1
- package/__tests__/publicWwwTest/cartella vuota con spazi nel nome /file con spazio nel nome .txt +0 -1
- package/__tests__/publicWwwTest/ejs-templates/complex.ejs +0 -56
- package/__tests__/publicWwwTest/ejs-templates/index.ejs +0 -30
- package/__tests__/publicWwwTest/ejs-templates/simple.ejs +0 -13
- package/__tests__/publicWwwTest/ejs-templates/with-conditional.ejs +0 -28
- package/__tests__/publicWwwTest/ejs-templates/with-escaping.ejs +0 -26
- package/__tests__/publicWwwTest/ejs-templates/with-loop.ejs +0 -16
- package/__tests__/publicWwwTest/hideext-test/about/index.html +0 -1
- package/__tests__/publicWwwTest/hideext-test/about.EJS +0 -1
- package/__tests__/publicWwwTest/hideext-test/about.ejs +0 -2
- package/__tests__/publicWwwTest/hideext-test/blog/articolo.ejs +0 -2
- package/__tests__/publicWwwTest/hideext-test/conflict-test/pagina +0 -1
- package/__tests__/publicWwwTest/hideext-test/conflict-test/pagina.ejs +0 -1
- package/__tests__/publicWwwTest/hideext-test/file.ejs.bak +0 -1
- package/__tests__/publicWwwTest/hideext-test/index.ejs +0 -2
- package/__tests__/publicWwwTest/hideext-test/photo.txt +0 -1
- package/__tests__/publicWwwTest/hideext-test/sezione/index.ejs +0 -1
- package/__tests__/publicWwwTest/hideext-test/style.css +0 -1
- package/__tests__/publicWwwTest/ile_vuoto.txt +0 -0
- package/__tests__/publicWwwTest/index.html +0 -11
- package/__tests__/publicWwwTest/prova file .txt +0 -2
- package/__tests__/publicWwwTest/semplicetxt.txt +0 -1
- package/__tests__/publicWwwTest/test-page.html +0 -1
- package/__tests__/publicWwwTest/test.txt +0 -1
- package/__tests__/range-fixtures/sample.txt +0 -1
- package/__tests__/range.test.js +0 -223
- package/__tests__/security-headers.test.js +0 -165
- package/__tests__/security.test.js +0 -322
- package/__tests__/server-cache-fixtures/large.txt +0 -1
- package/__tests__/server-cache-fixtures/small.txt +0 -1
- package/__tests__/server-cache.test.js +0 -594
- package/__tests__/setup-benchmark.js +0 -178
- package/__tests__/symlink.test.js +0 -441
- package/__tests__/template-timeout.test.js +0 -321
- package/__tests__/test-regex-quick.js +0 -158
- package/__tests__/useOriginalUrl.test.js +0 -213
- package/docs/ACTION_PLAN.md +0 -293
- package/docs/BENCHMARKS.md +0 -317
- package/docs/CHANGELOG.md +0 -906
- package/docs/CODE_REVIEW.md +0 -300
- package/docs/DEBUG_REPORT.md +0 -593
- package/docs/DOCUMENTATION.md +0 -1864
- package/docs/EXAMPLES_INDEX_OPTION.md +0 -395
- package/docs/FLOW_DIAGRAM.md +0 -954
- package/docs/INDEX_OPTION_PRIORITY.md +0 -527
- package/docs/OPTIMIZATION_HTTP_CACHING.md +0 -689
- package/docs/OPTIMIZATION_ROADMAP_for_V3.md +0 -864
- package/docs/PERFORMANCE_ANALYSIS.md +0 -839
- package/docs/PERFORMANCE_COMPARISON.md +0 -388
- package/docs/noteExports.md +0 -148
- package/docs/security_improvement_for_V3.md +0 -421
- package/docs/template-engine/TEMPLATE_ENGINE_GUIDE.md +0 -1734
- package/docs/template-engine/esempi-incrementali.js +0 -192
- package/docs/template-engine/examples/esempio1-nessun-dato.ejs +0 -12
- package/docs/template-engine/examples/esempio2-una-variabile.ejs +0 -11
- package/docs/template-engine/examples/esempio3-piu-variabili.ejs +0 -15
- package/docs/template-engine/examples/esempio4-condizionale.ejs +0 -18
- package/docs/template-engine/examples/esempio5-loop.ejs +0 -18
- package/docs/template-engine/examples/index-esempi.html +0 -181
- package/docs/template-engine/examples/index.html +0 -40
- package/docs/template-engine/examples/test.ejs +0 -64
- package/eslint.config.mjs +0 -17
- package/jest.config.js +0 -18
package/__tests__/ejs.test.js
DELETED
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
const Koa = require('koa');
|
|
2
|
-
const koaClassicServer = require('../index.cjs');
|
|
3
|
-
const supertest = require('supertest');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const ejs = require('ejs');
|
|
7
|
-
|
|
8
|
-
describe('EJS Template Engine Integration Tests', () => {
|
|
9
|
-
let app;
|
|
10
|
-
let server;
|
|
11
|
-
let request;
|
|
12
|
-
|
|
13
|
-
beforeAll(() => {
|
|
14
|
-
app = new Koa();
|
|
15
|
-
|
|
16
|
-
const rootDir = path.join(__dirname, 'publicWwwTest');
|
|
17
|
-
|
|
18
|
-
// Configure koaClassicServer with EJS template support
|
|
19
|
-
app.use(
|
|
20
|
-
koaClassicServer(rootDir, {
|
|
21
|
-
method: ['GET'],
|
|
22
|
-
dirListing: { enabled: true },
|
|
23
|
-
template: {
|
|
24
|
-
ext: ['ejs'],
|
|
25
|
-
render: async (ctx, next, filePath) => {
|
|
26
|
-
// Read the template file
|
|
27
|
-
const templateContent = await fs.promises.readFile(filePath, 'utf-8');
|
|
28
|
-
|
|
29
|
-
// Prepare data for different templates
|
|
30
|
-
const data = getTemplateData(filePath);
|
|
31
|
-
|
|
32
|
-
// Render with EJS
|
|
33
|
-
const html = ejs.render(templateContent, data);
|
|
34
|
-
|
|
35
|
-
ctx.type = 'text/html';
|
|
36
|
-
ctx.body = html;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
})
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
server = app.listen();
|
|
43
|
-
request = supertest(server);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
afterAll(() => {
|
|
47
|
-
if (server) {
|
|
48
|
-
server.close();
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// Helper function to provide data for each template
|
|
53
|
-
function getTemplateData(filePath) {
|
|
54
|
-
const basename = path.basename(filePath, '.ejs');
|
|
55
|
-
|
|
56
|
-
const dataMap = {
|
|
57
|
-
'simple': {
|
|
58
|
-
title: 'Simple EJS Test',
|
|
59
|
-
heading: 'Hello from EJS',
|
|
60
|
-
message: 'This is a simple template test',
|
|
61
|
-
timestamp: '2025-11-18'
|
|
62
|
-
},
|
|
63
|
-
'with-loop': {
|
|
64
|
-
items: [
|
|
65
|
-
{ name: 'Item 1', value: 100 },
|
|
66
|
-
{ name: 'Item 2', value: 200 },
|
|
67
|
-
{ name: 'Item 3', value: 300 }
|
|
68
|
-
]
|
|
69
|
-
},
|
|
70
|
-
'with-conditional': {
|
|
71
|
-
isLoggedIn: true,
|
|
72
|
-
username: 'TestUser',
|
|
73
|
-
role: 'admin',
|
|
74
|
-
notifications: 5
|
|
75
|
-
},
|
|
76
|
-
'with-escaping': {
|
|
77
|
-
userInput: '<script>alert("XSS")</script>',
|
|
78
|
-
htmlContent: '<strong>Bold text</strong>',
|
|
79
|
-
safeText: 'Plain & safe text',
|
|
80
|
-
allowedHtml: '<em>Emphasized text</em>'
|
|
81
|
-
},
|
|
82
|
-
'complex': {
|
|
83
|
-
pageTitle: 'E-Commerce Products',
|
|
84
|
-
user: {
|
|
85
|
-
name: 'Mario Rossi'
|
|
86
|
-
},
|
|
87
|
-
products: [
|
|
88
|
-
{
|
|
89
|
-
id: 1,
|
|
90
|
-
name: 'Laptop',
|
|
91
|
-
description: 'High-performance laptop',
|
|
92
|
-
price: 999.99,
|
|
93
|
-
discount: 10,
|
|
94
|
-
inStock: true
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
id: 2,
|
|
98
|
-
name: 'Mouse',
|
|
99
|
-
description: 'Wireless mouse',
|
|
100
|
-
price: 29.99,
|
|
101
|
-
discount: 0,
|
|
102
|
-
inStock: true
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
id: 3,
|
|
106
|
-
name: 'Keyboard',
|
|
107
|
-
description: 'Mechanical keyboard',
|
|
108
|
-
price: 149.99,
|
|
109
|
-
discount: 15,
|
|
110
|
-
inStock: false
|
|
111
|
-
}
|
|
112
|
-
]
|
|
113
|
-
},
|
|
114
|
-
'testEjs': {
|
|
115
|
-
// For the existing test file
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
return dataMap[basename] || {};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
describe('Simple Template - simple.ejs', () => {
|
|
123
|
-
test('Should render simple template with variables', async () => {
|
|
124
|
-
const response = await request.get('/ejs-templates/simple.ejs');
|
|
125
|
-
|
|
126
|
-
expect(response.status).toBe(200);
|
|
127
|
-
expect(response.type).toBe('text/html');
|
|
128
|
-
expect(response.text).toContain('Simple EJS Test');
|
|
129
|
-
expect(response.text).toContain('Hello from EJS');
|
|
130
|
-
expect(response.text).toContain('This is a simple template test');
|
|
131
|
-
expect(response.text).toContain('2025-11-18');
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
describe('Loop Template - with-loop.ejs', () => {
|
|
136
|
-
test('Should render template with forEach loop', async () => {
|
|
137
|
-
const response = await request.get('/ejs-templates/with-loop.ejs');
|
|
138
|
-
|
|
139
|
-
expect(response.status).toBe(200);
|
|
140
|
-
expect(response.text).toContain('Lista di Items');
|
|
141
|
-
expect(response.text).toContain('Item 1 - 100');
|
|
142
|
-
expect(response.text).toContain('Item 2 - 200');
|
|
143
|
-
expect(response.text).toContain('Item 3 - 300');
|
|
144
|
-
expect(response.text).toContain('Totale items: 3');
|
|
145
|
-
expect(response.text).toContain('data-index="0"');
|
|
146
|
-
expect(response.text).toContain('data-index="1"');
|
|
147
|
-
expect(response.text).toContain('data-index="2"');
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
describe('Conditional Template - with-conditional.ejs', () => {
|
|
152
|
-
test('Should render template with if/else conditionals (logged in)', async () => {
|
|
153
|
-
const response = await request.get('/ejs-templates/with-conditional.ejs');
|
|
154
|
-
|
|
155
|
-
expect(response.status).toBe(200);
|
|
156
|
-
expect(response.text).toContain('Benvenuto, TestUser!');
|
|
157
|
-
expect(response.text).toContain('Ruolo: admin');
|
|
158
|
-
expect(response.text).toContain('Hai 5 nuove notifiche');
|
|
159
|
-
// Should NOT contain guest panel
|
|
160
|
-
expect(response.text).not.toContain('Benvenuto, ospite!');
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
describe('Escaping Template - with-escaping.ejs', () => {
|
|
165
|
-
test('Should properly escape HTML in <%= %> tags', async () => {
|
|
166
|
-
const response = await request.get('/ejs-templates/with-escaping.ejs');
|
|
167
|
-
|
|
168
|
-
expect(response.status).toBe(200);
|
|
169
|
-
|
|
170
|
-
// HTML should be escaped (safe)
|
|
171
|
-
// Note: EJS uses " instead of " for quotes
|
|
172
|
-
expect(response.text).toContain('<script>alert("XSS")</script>');
|
|
173
|
-
|
|
174
|
-
// HTML should NOT be escaped (unsafe - unescaped output)
|
|
175
|
-
expect(response.text).toContain('<strong>Bold text</strong>');
|
|
176
|
-
|
|
177
|
-
// Safe text with & should be escaped
|
|
178
|
-
expect(response.text).toContain('Plain & safe text');
|
|
179
|
-
|
|
180
|
-
// Allowed HTML should be rendered
|
|
181
|
-
expect(response.text).toContain('<em>Emphasized text</em>');
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test('Should protect against XSS attacks', async () => {
|
|
185
|
-
const response = await request.get('/ejs-templates/with-escaping.ejs');
|
|
186
|
-
|
|
187
|
-
// The escaped section should not contain executable script tags
|
|
188
|
-
const escapedSection = response.text.match(/<div class="escaped">[\s\S]*?<\/div>/)[0];
|
|
189
|
-
expect(escapedSection).not.toContain('<script>');
|
|
190
|
-
expect(escapedSection).toContain('<script>');
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
describe('Complex Template - complex.ejs', () => {
|
|
195
|
-
test('Should render complex template with all features', async () => {
|
|
196
|
-
const response = await request.get('/ejs-templates/complex.ejs');
|
|
197
|
-
|
|
198
|
-
expect(response.status).toBe(200);
|
|
199
|
-
|
|
200
|
-
// Page title
|
|
201
|
-
expect(response.text).toContain('E-Commerce Products');
|
|
202
|
-
|
|
203
|
-
// User greeting
|
|
204
|
-
expect(response.text).toContain('Ciao, Mario Rossi!');
|
|
205
|
-
|
|
206
|
-
// Products list
|
|
207
|
-
expect(response.text).toContain('Laptop');
|
|
208
|
-
expect(response.text).toContain('High-performance laptop');
|
|
209
|
-
expect(response.text).toContain('€999.99');
|
|
210
|
-
|
|
211
|
-
// Discount calculation
|
|
212
|
-
expect(response.text).toContain('Sconto: 10%');
|
|
213
|
-
expect(response.text).toContain('€899.99'); // 999.99 - 10%
|
|
214
|
-
|
|
215
|
-
// Mouse without discount
|
|
216
|
-
expect(response.text).toContain('Mouse');
|
|
217
|
-
expect(response.text).toContain('€29.99');
|
|
218
|
-
|
|
219
|
-
// Keyboard out of stock
|
|
220
|
-
expect(response.text).toContain('Keyboard');
|
|
221
|
-
expect(response.text).toContain('Non disponibile');
|
|
222
|
-
|
|
223
|
-
// Total count
|
|
224
|
-
expect(response.text).toContain('Totale prodotti: 3');
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
test('Should have correct product data attributes', async () => {
|
|
228
|
-
const response = await request.get('/ejs-templates/complex.ejs');
|
|
229
|
-
|
|
230
|
-
expect(response.text).toContain('data-id="1"');
|
|
231
|
-
expect(response.text).toContain('data-id="2"');
|
|
232
|
-
expect(response.text).toContain('data-id="3"');
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
test('Should show "Add to cart" button only for in-stock items', async () => {
|
|
236
|
-
const response = await request.get('/ejs-templates/complex.ejs');
|
|
237
|
-
|
|
238
|
-
// Count "Aggiungi al carrello" buttons - should be 2 (Laptop and Mouse)
|
|
239
|
-
const buttonMatches = response.text.match(/Aggiungi al carrello/g);
|
|
240
|
-
expect(buttonMatches).toHaveLength(2);
|
|
241
|
-
|
|
242
|
-
// Count "Non disponibile" - should be 1 (Keyboard)
|
|
243
|
-
const outOfStockMatches = response.text.match(/Non disponibile/g);
|
|
244
|
-
expect(outOfStockMatches).toHaveLength(1);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
describe('Index Template - index.ejs', () => {
|
|
249
|
-
test('Should render index page with links', async () => {
|
|
250
|
-
const response = await request.get('/ejs-templates/index.ejs');
|
|
251
|
-
|
|
252
|
-
expect(response.status).toBe(200);
|
|
253
|
-
expect(response.text).toContain('Test Templates EJS');
|
|
254
|
-
expect(response.text).toContain('simple.ejs');
|
|
255
|
-
expect(response.text).toContain('with-loop.ejs');
|
|
256
|
-
expect(response.text).toContain('with-conditional.ejs');
|
|
257
|
-
expect(response.text).toContain('with-escaping.ejs');
|
|
258
|
-
expect(response.text).toContain('complex.ejs');
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
describe('Existing EJS file - testEjs.ejs', () => {
|
|
263
|
-
test('Should render existing testEjs.ejs file', async () => {
|
|
264
|
-
const response = await request.get('/cartella/sottocartella/provaEjs/testEjs.ejs');
|
|
265
|
-
|
|
266
|
-
expect(response.status).toBe(200);
|
|
267
|
-
expect(response.type).toBe('text/html');
|
|
268
|
-
expect(response.text).toContain('<h1>hello world</h1>');
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
describe('Error Handling', () => {
|
|
273
|
-
test('Should return 404 for non-existent .ejs file', async () => {
|
|
274
|
-
const response = await request.get('/ejs-templates/non-existent.ejs');
|
|
275
|
-
|
|
276
|
-
expect(response.status).toBe(404);
|
|
277
|
-
});
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
describe('Performance', () => {
|
|
281
|
-
test('Should render complex template in reasonable time', async () => {
|
|
282
|
-
const start = Date.now();
|
|
283
|
-
const response = await request.get('/ejs-templates/complex.ejs');
|
|
284
|
-
const duration = Date.now() - start;
|
|
285
|
-
|
|
286
|
-
expect(response.status).toBe(200);
|
|
287
|
-
expect(duration).toBeLessThan(100); // Should render in less than 100ms
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
test('Should render simple template very quickly', async () => {
|
|
291
|
-
const start = Date.now();
|
|
292
|
-
const response = await request.get('/ejs-templates/simple.ejs');
|
|
293
|
-
const duration = Date.now() - start;
|
|
294
|
-
|
|
295
|
-
expect(response.status).toBe(200);
|
|
296
|
-
expect(duration).toBeLessThan(50); // Should render in less than 50ms
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
});
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
const Koa = require('koa');
|
|
2
|
-
const koaClassicServer = require('../index.cjs');
|
|
3
|
-
const supertest = require('supertest');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const ejs = require('ejs');
|
|
7
|
-
|
|
8
|
-
// Regression tests for the HEAD-method HTTP-conformance bug.
|
|
9
|
-
//
|
|
10
|
-
// RFC 9110 §9.3.2: a HEAD response must be identical to the GET response for the
|
|
11
|
-
// same resource, minus the message body — same status code, same headers
|
|
12
|
-
// (notably Content-Type and Content-Length).
|
|
13
|
-
//
|
|
14
|
-
// Before the fix, a route served by the template engine returned 200 on GET but
|
|
15
|
-
// 404 on HEAD whenever the operator's render function did not itself handle HEAD
|
|
16
|
-
// (e.g. it returned early on non-GET requests). The static-file branch already
|
|
17
|
-
// handled HEAD correctly, and directory listings happened to work because Koa
|
|
18
|
-
// strips the string body for HEAD — only the template branch was broken.
|
|
19
|
-
|
|
20
|
-
const ROOT = path.join(__dirname, 'publicWwwTest');
|
|
21
|
-
|
|
22
|
-
// Minimal data for the templates exercised here. ejs-templates/index.ejs needs no
|
|
23
|
-
// variables; simple.ejs needs these four. Anything else renders with no locals.
|
|
24
|
-
function templateData(filePath) {
|
|
25
|
-
if (path.basename(filePath, '.ejs') === 'simple') {
|
|
26
|
-
return {
|
|
27
|
-
title: 'Simple EJS Test',
|
|
28
|
-
heading: 'Hello from EJS',
|
|
29
|
-
message: 'This is a simple template test',
|
|
30
|
-
timestamp: '2025-11-18',
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
return {};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// A method-AWARE render: it only produces a body on GET. This is a common
|
|
37
|
-
// real-world pattern (operators guard render work behind a GET check) and is
|
|
38
|
-
// exactly what exposed the bug — on HEAD it never set ctx.body, so the status
|
|
39
|
-
// stayed at Koa's default 404. This render is what makes the regression real:
|
|
40
|
-
// with a naive render the test would pass even without the fix, because Koa
|
|
41
|
-
// strips the body of a 200 GET response for HEAD on its own.
|
|
42
|
-
const methodAwareRender = async (ctx, next, filePath) => {
|
|
43
|
-
if (ctx.method !== 'GET') return;
|
|
44
|
-
const tpl = await fs.promises.readFile(filePath, 'utf-8');
|
|
45
|
-
ctx.type = 'text/html';
|
|
46
|
-
ctx.body = ejs.render(tpl, templateData(filePath));
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
// A method-AGNOSTIC render: sets the body regardless of method. Proves the fix
|
|
50
|
-
// does not regress renders that already worked.
|
|
51
|
-
const naiveRender = async (ctx, next, filePath) => {
|
|
52
|
-
const tpl = await fs.promises.readFile(filePath, 'utf-8');
|
|
53
|
-
ctx.type = 'text/html';
|
|
54
|
-
ctx.body = ejs.render(tpl, templateData(filePath));
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
function buildServer(render) {
|
|
58
|
-
const app = new Koa();
|
|
59
|
-
app.use(
|
|
60
|
-
koaClassicServer(ROOT, {
|
|
61
|
-
method: ['GET', 'HEAD'],
|
|
62
|
-
index: ['index.html', 'index.ejs'],
|
|
63
|
-
dirListing: { enabled: true },
|
|
64
|
-
template: { ext: ['ejs'], render },
|
|
65
|
-
})
|
|
66
|
-
);
|
|
67
|
-
return app.listen();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Asserts the HEAD response for `urlPath` matches the corresponding GET in status,
|
|
71
|
-
// Content-Type and Content-Length, and that HEAD carries no body.
|
|
72
|
-
async function expectHeadMatchesGet(request, urlPath, expectedStatus) {
|
|
73
|
-
const get = await request.get(urlPath);
|
|
74
|
-
const head = await request.head(urlPath);
|
|
75
|
-
|
|
76
|
-
expect(get.status).toBe(expectedStatus);
|
|
77
|
-
expect(head.status).toBe(expectedStatus);
|
|
78
|
-
|
|
79
|
-
// Same headers as GET (RFC 9110 §9.3.2)
|
|
80
|
-
expect(head.headers['content-type']).toBe(get.headers['content-type']);
|
|
81
|
-
expect(head.headers['content-length']).toBe(get.headers['content-length']);
|
|
82
|
-
// Content-Length must actually be populated for a body-bearing response
|
|
83
|
-
expect(head.headers['content-length']).toBeDefined();
|
|
84
|
-
|
|
85
|
-
// HEAD must not send a body (supertest surfaces an empty object for HEAD)
|
|
86
|
-
expect(head.text).toBeFalsy();
|
|
87
|
-
expect(head.body).toEqual({});
|
|
88
|
-
|
|
89
|
-
return { get, head };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
describe('HEAD method — template engine routes (method-aware render)', () => {
|
|
93
|
-
let server;
|
|
94
|
-
let request;
|
|
95
|
-
beforeAll(() => { server = buildServer(methodAwareRender); request = supertest(server); });
|
|
96
|
-
afterAll(() => server.close());
|
|
97
|
-
|
|
98
|
-
// The headline bug: a directory whose index is a template (index.ejs as the
|
|
99
|
-
// index of /ejs-templates/). GET 200, HEAD must also be 200 — not 404.
|
|
100
|
-
test('HEAD on a directory whose index is a template → 200, matches GET', async () => {
|
|
101
|
-
const { get } = await expectHeadMatchesGet(request, '/ejs-templates/', 200);
|
|
102
|
-
// sanity: GET really did render the template
|
|
103
|
-
expect(get.text).toContain('Test Templates EJS');
|
|
104
|
-
expect(get.headers['content-type']).toMatch(/text\/html/);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('HEAD on a directly-requested template file → 200, matches GET', async () => {
|
|
108
|
-
const { get } = await expectHeadMatchesGet(request, '/ejs-templates/simple.ejs', 200);
|
|
109
|
-
expect(get.text).toContain('Hello from EJS');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test('HEAD on a static file → 200, matches GET', async () => {
|
|
113
|
-
const { head } = await expectHeadMatchesGet(request, '/test.txt', 200);
|
|
114
|
-
// static branch advertises range support
|
|
115
|
-
expect(head.headers['accept-ranges']).toBe('bytes');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test('HEAD on a listable directory (no index) → 200, matches GET', async () => {
|
|
119
|
-
const { get } = await expectHeadMatchesGet(request, '/cartella/', 200);
|
|
120
|
-
expect(get.headers['content-type']).toMatch(/text\/html/);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test('HEAD on a non-existent template path → 404, matches GET', async () => {
|
|
124
|
-
const get = await request.get('/ejs-templates/non-existent.ejs');
|
|
125
|
-
const head = await request.head('/ejs-templates/non-existent.ejs');
|
|
126
|
-
expect(get.status).toBe(404);
|
|
127
|
-
expect(head.status).toBe(404);
|
|
128
|
-
expect(head.headers['content-type']).toBe(get.headers['content-type']);
|
|
129
|
-
expect(head.text).toBeFalsy();
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test('HEAD on a non-existent static path → 404, matches GET', async () => {
|
|
133
|
-
const get = await request.get('/does-not-exist.txt');
|
|
134
|
-
const head = await request.head('/does-not-exist.txt');
|
|
135
|
-
expect(get.status).toBe(404);
|
|
136
|
-
expect(head.status).toBe(404);
|
|
137
|
-
expect(head.text).toBeFalsy();
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
describe('HEAD method — template engine routes (method-agnostic render, no regression)', () => {
|
|
142
|
-
let server;
|
|
143
|
-
let request;
|
|
144
|
-
beforeAll(() => { server = buildServer(naiveRender); request = supertest(server); });
|
|
145
|
-
afterAll(() => server.close());
|
|
146
|
-
|
|
147
|
-
test('HEAD on template index still matches GET → 200', async () => {
|
|
148
|
-
await expectHeadMatchesGet(request, '/ejs-templates/', 200);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test('HEAD on direct template still matches GET → 200', async () => {
|
|
152
|
-
await expectHeadMatchesGet(request, '/ejs-templates/simple.ejs', 200);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test('HEAD on non-existent template → 404', async () => {
|
|
156
|
-
const head = await request.head('/ejs-templates/non-existent.ejs');
|
|
157
|
-
expect(head.status).toBe(404);
|
|
158
|
-
expect(head.text).toBeFalsy();
|
|
159
|
-
});
|
|
160
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
dot dir file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
acme
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
password
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
key data
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
secret data
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
regular
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
normal
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
NESTED_SECRET=nestedvalue
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
regular in subdir
|