koa-classic-server 2.0.0 → 2.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 +31 -17
- package/__tests__/directory-sorting-links.test.js +135 -0
- package/__tests__/ejs.test.js +299 -0
- package/__tests__/performance.test.js +75 -6
- package/__tests__/publicWwwTest/ejs-templates/complex.ejs +56 -0
- package/__tests__/publicWwwTest/ejs-templates/index.ejs +30 -0
- package/__tests__/publicWwwTest/ejs-templates/simple.ejs +13 -0
- package/__tests__/publicWwwTest/ejs-templates/with-conditional.ejs +28 -0
- package/__tests__/publicWwwTest/ejs-templates/with-escaping.ejs +26 -0
- package/__tests__/publicWwwTest/ejs-templates/with-loop.ejs +16 -0
- package/{scripts → __tests__}/setup-benchmark.js +1 -1
- package/docs/CODE_REVIEW.md +298 -0
- package/docs/FLOW_DIAGRAM.md +952 -0
- package/docs/template-engine/TEMPLATE_ENGINE_GUIDE.md +1734 -0
- package/docs/template-engine/esempi-incrementali.js +192 -0
- package/docs/template-engine/examples/esempio1-nessun-dato.ejs +12 -0
- package/docs/template-engine/examples/esempio2-una-variabile.ejs +11 -0
- package/docs/template-engine/examples/esempio3-piu-variabili.ejs +15 -0
- package/docs/template-engine/examples/esempio4-condizionale.ejs +18 -0
- package/docs/template-engine/examples/esempio5-loop.ejs +18 -0
- package/docs/template-engine/examples/index-esempi.html +181 -0
- package/docs/template-engine/examples/index.html +40 -0
- package/docs/template-engine/examples/test.ejs +64 -0
- package/index.cjs +186 -35
- package/package.json +9 -6
- package/CREATE_RELEASE.sh +0 -53
- package/publish-to-npm.sh +0 -65
- /package/{benchmark-results-baseline-v1.2.0.txt → __tests__/benchmark-results-baseline-v1.2.0.txt} +0 -0
- /package/{benchmark-results-optimized-v2.0.0.txt → __tests__/benchmark-results-optimized-v2.0.0.txt} +0 -0
- /package/{benchmark.js → __tests__/benchmark.js} +0 -0
- /package/{customTest → __tests__/customTest}/README.md +0 -0
- /package/{customTest → __tests__/customTest}/loadConfig.util.js +0 -0
- /package/{customTest → __tests__/customTest}/serversToLoad.util.js +0 -0
- /package/{demo-regex-index.js → __tests__/demo-regex-index.js} +0 -0
- /package/{test-regex-quick.js → __tests__/test-regex-quick.js} +0 -0
- /package/{BENCHMARKS.md → docs/BENCHMARKS.md} +0 -0
- /package/{CHANGELOG.md → docs/CHANGELOG.md} +0 -0
- /package/{DEBUG_REPORT.md → docs/DEBUG_REPORT.md} +0 -0
- /package/{DOCUMENTATION.md → docs/DOCUMENTATION.md} +0 -0
- /package/{EXAMPLES_INDEX_OPTION.md → docs/EXAMPLES_INDEX_OPTION.md} +0 -0
- /package/{INDEX_OPTION_PRIORITY.md → docs/INDEX_OPTION_PRIORITY.md} +0 -0
- /package/{OPTIMIZATION_HTTP_CACHING.md → docs/OPTIMIZATION_HTTP_CACHING.md} +0 -0
- /package/{PERFORMANCE_ANALYSIS.md → docs/PERFORMANCE_ANALYSIS.md} +0 -0
- /package/{PERFORMANCE_COMPARISON.md → docs/PERFORMANCE_COMPARISON.md} +0 -0
- /package/{noteExports.md → docs/noteExports.md} +0 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/koa-classic-server)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
|
-
[]()
|
|
8
8
|
|
|
9
9
|
## ⚠️ Version 1.2.0 - Critical Security Update
|
|
10
10
|
|
|
@@ -17,9 +17,9 @@ Version 1.2.0 includes **critical security fixes** for path traversal vulnerabil
|
|
|
17
17
|
✅ **Template Error Handling** - No more server crashes
|
|
18
18
|
✅ **XSS Protection** - HTML escaping in directory listings
|
|
19
19
|
✅ **Race Condition Fixes** - Robust file access
|
|
20
|
-
✅ **
|
|
20
|
+
✅ **146 Tests Passing** - Comprehensive test coverage including EJS integration
|
|
21
21
|
|
|
22
|
-
[See full changelog](./CHANGELOG.md)
|
|
22
|
+
[See full changelog](./docs/CHANGELOG.md)
|
|
23
23
|
|
|
24
24
|
## Features
|
|
25
25
|
|
|
@@ -32,7 +32,7 @@ koa-classic-server is a middleware for serving static files from a directory wit
|
|
|
32
32
|
- 🎨 **Template Engine Support** - Integrate EJS, Pug, Handlebars, etc.
|
|
33
33
|
- 🔒 **Security** - Path traversal protection, XSS prevention
|
|
34
34
|
- ⚙️ **Configurable** - URL prefixes, reserved paths, index files
|
|
35
|
-
- 🧪 **Well-Tested** -
|
|
35
|
+
- 🧪 **Well-Tested** - 146 tests with comprehensive coverage (security, EJS, performance)
|
|
36
36
|
- 📦 **Dual Module Support** - CommonJS and ES Modules
|
|
37
37
|
|
|
38
38
|
## Installation
|
|
@@ -110,24 +110,36 @@ app.listen(3000);
|
|
|
110
110
|
const Koa = require('koa');
|
|
111
111
|
const koaClassicServer = require('koa-classic-server');
|
|
112
112
|
const ejs = require('ejs');
|
|
113
|
+
const fs = require('fs').promises;
|
|
113
114
|
|
|
114
115
|
const app = new Koa();
|
|
115
116
|
|
|
116
117
|
app.use(koaClassicServer(__dirname + '/views', {
|
|
117
118
|
template: {
|
|
119
|
+
ext: ['ejs'], // File extensions to process
|
|
118
120
|
render: async (ctx, next, filePath) => {
|
|
119
|
-
|
|
121
|
+
// Read template file
|
|
122
|
+
const templateContent = await fs.readFile(filePath, 'utf-8');
|
|
123
|
+
|
|
124
|
+
// Render with data
|
|
125
|
+
const html = ejs.render(templateContent, {
|
|
120
126
|
title: 'My App',
|
|
121
|
-
user: ctx.state.user
|
|
127
|
+
user: ctx.state.user || { name: 'Guest' },
|
|
128
|
+
path: ctx.path,
|
|
129
|
+
timestamp: new Date().toISOString()
|
|
122
130
|
});
|
|
123
|
-
|
|
124
|
-
|
|
131
|
+
|
|
132
|
+
ctx.type = 'text/html';
|
|
133
|
+
ctx.body = html;
|
|
134
|
+
}
|
|
125
135
|
}
|
|
126
136
|
}));
|
|
127
137
|
|
|
128
138
|
app.listen(3000);
|
|
129
139
|
```
|
|
130
140
|
|
|
141
|
+
See **[Template Engine Guide](./docs/template-engine/TEMPLATE_ENGINE_GUIDE.md)** for comprehensive template engine documentation with progressive examples.
|
|
142
|
+
|
|
131
143
|
## API
|
|
132
144
|
|
|
133
145
|
### koaClassicServer(rootDir, options)
|
|
@@ -282,11 +294,14 @@ See [CHANGELOG.md](./CHANGELOG.md) for detailed information.
|
|
|
282
294
|
|
|
283
295
|
For complete documentation with all features, examples, troubleshooting, and best practices, see:
|
|
284
296
|
|
|
285
|
-
- **[DOCUMENTATION.md](./DOCUMENTATION.md)** - Complete API reference and usage guide
|
|
286
|
-
- **[
|
|
287
|
-
- **[
|
|
288
|
-
- **[
|
|
289
|
-
- **[
|
|
297
|
+
- **[DOCUMENTATION.md](./docs/DOCUMENTATION.md)** - Complete API reference and usage guide
|
|
298
|
+
- **[FLOW_DIAGRAM.md](./docs/FLOW_DIAGRAM.md)** - Visual flow diagrams and code execution paths
|
|
299
|
+
- **[TEMPLATE_ENGINE_GUIDE.md](./docs/template-engine/TEMPLATE_ENGINE_GUIDE.md)** - Complete guide to template engine integration (EJS, Pug, Handlebars, Nunjucks)
|
|
300
|
+
- **[INDEX_OPTION_PRIORITY.md](./docs/INDEX_OPTION_PRIORITY.md)** - Detailed priority behavior for `index` option (string, array, RegExp)
|
|
301
|
+
- **[EXAMPLES_INDEX_OPTION.md](./docs/EXAMPLES_INDEX_OPTION.md)** - 10 practical examples of `index` option with RegExp
|
|
302
|
+
- **[PERFORMANCE_ANALYSIS.md](./docs/PERFORMANCE_ANALYSIS.md)** - Performance optimization analysis
|
|
303
|
+
- **[PERFORMANCE_COMPARISON.md](./docs/PERFORMANCE_COMPARISON.md)** - Before/after performance benchmarks
|
|
304
|
+
- **[CODE_REVIEW.md](./docs/CODE_REVIEW.md)** - Code quality analysis and review
|
|
290
305
|
|
|
291
306
|
## Contributing
|
|
292
307
|
|
|
@@ -295,9 +310,8 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
|
|
295
310
|
## Known Limitations
|
|
296
311
|
|
|
297
312
|
- Reserved URLs only work for first-level directories
|
|
298
|
-
- Single index file name (no fallback array)
|
|
299
313
|
|
|
300
|
-
See [DEBUG_REPORT.md](./DEBUG_REPORT.md) for technical details.
|
|
314
|
+
See [DEBUG_REPORT.md](./docs/DEBUG_REPORT.md) for technical details.
|
|
301
315
|
|
|
302
316
|
## License
|
|
303
317
|
|
|
@@ -313,8 +327,8 @@ See [CHANGELOG.md](./CHANGELOG.md)
|
|
|
313
327
|
|
|
314
328
|
## Links
|
|
315
329
|
|
|
316
|
-
- [Full Documentation](./DOCUMENTATION.md)
|
|
317
|
-
- [Debug Report](./DEBUG_REPORT.md)
|
|
330
|
+
- [Full Documentation](./docs/DOCUMENTATION.md)
|
|
331
|
+
- [Debug Report](./docs/DEBUG_REPORT.md)
|
|
318
332
|
- [Changelog](./CHANGELOG.md)
|
|
319
333
|
- [Repository](https://github.com/italopaesano/koa-classic-server)
|
|
320
334
|
- [npm Package](https://www.npmjs.com/package/koa-classic-server)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const Koa = require('koa');
|
|
2
|
+
const request = require('supertest');
|
|
3
|
+
const koaClassicServer = require('../index.cjs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
describe('Directory Sorting Links Bug Tests', () => {
|
|
7
|
+
let app;
|
|
8
|
+
let server;
|
|
9
|
+
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
app = new Koa();
|
|
12
|
+
const publicDir = path.join(__dirname, 'publicWwwTest');
|
|
13
|
+
|
|
14
|
+
app.use(koaClassicServer(publicDir, {
|
|
15
|
+
method: ['GET'],
|
|
16
|
+
showDirContents: true
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
server = app.listen();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterAll(() => {
|
|
23
|
+
server.close();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('Bug: Links with query parameters in path', () => {
|
|
27
|
+
test('File links should not contain sort query parameters in path', async () => {
|
|
28
|
+
const response = await request(server)
|
|
29
|
+
.get('/?sort=name&order=asc')
|
|
30
|
+
.expect(200);
|
|
31
|
+
|
|
32
|
+
console.log('Testing for bug...');
|
|
33
|
+
console.log('Looking for malformed URLs like: ?sort=name&order=asc/');
|
|
34
|
+
|
|
35
|
+
// The bug creates malformed URLs like: href="http://localhost:3000/?sort=name&order=asc/test.txt"
|
|
36
|
+
// Should NOT contain query params before slash
|
|
37
|
+
expect(response.text).not.toContain('?sort=name&order=asc/');
|
|
38
|
+
|
|
39
|
+
// Also check specific pattern
|
|
40
|
+
const malformedPattern = /href="[^"]*\?sort=[^"]*\//;
|
|
41
|
+
expect(response.text).not.toMatch(malformedPattern);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('Directory links should not contain query parameters in path', async () => {
|
|
45
|
+
const response = await request(server)
|
|
46
|
+
.get('/?sort=size&order=desc')
|
|
47
|
+
.expect(200);
|
|
48
|
+
|
|
49
|
+
// Bug would create: href="http://localhost/?sort=size&order=desc/cartella"
|
|
50
|
+
expect(response.text).not.toContain('?sort=size&order=desc/');
|
|
51
|
+
|
|
52
|
+
// Check for malformed href pattern
|
|
53
|
+
const malformedPattern = /href="[^"]*\?sort=[^"]*\//;
|
|
54
|
+
expect(response.text).not.toMatch(malformedPattern);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('Functional: Navigation after sorting', () => {
|
|
59
|
+
test('Should enter folder after sorting by name', async () => {
|
|
60
|
+
// Load root with sorting
|
|
61
|
+
const rootResponse = await request(server)
|
|
62
|
+
.get('/?sort=name&order=asc')
|
|
63
|
+
.expect(200);
|
|
64
|
+
|
|
65
|
+
expect(rootResponse.text).toContain('cartella');
|
|
66
|
+
|
|
67
|
+
// Enter cartella folder
|
|
68
|
+
const folderResponse = await request(server)
|
|
69
|
+
.get('/cartella')
|
|
70
|
+
.expect(200);
|
|
71
|
+
|
|
72
|
+
expect(folderResponse.text).toContain('sottocartella');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('Should navigate subfolders after sorting by size', async () => {
|
|
76
|
+
await request(server)
|
|
77
|
+
.get('/?sort=size&order=desc')
|
|
78
|
+
.expect(200);
|
|
79
|
+
|
|
80
|
+
// Enter cartella
|
|
81
|
+
const folderResponse = await request(server)
|
|
82
|
+
.get('/cartella')
|
|
83
|
+
.expect(200);
|
|
84
|
+
|
|
85
|
+
expect(folderResponse.text).toContain('sottocartella');
|
|
86
|
+
|
|
87
|
+
// Enter sottocartella
|
|
88
|
+
const subfolderResponse = await request(server)
|
|
89
|
+
.get('/cartella/sottocartella')
|
|
90
|
+
.expect(200);
|
|
91
|
+
|
|
92
|
+
expect(subfolderResponse.text).toContain('ciao.html');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('Should access file after sorting by type', async () => {
|
|
96
|
+
await request(server)
|
|
97
|
+
.get('/?sort=type&order=asc')
|
|
98
|
+
.expect(200);
|
|
99
|
+
|
|
100
|
+
// Access file
|
|
101
|
+
const fileResponse = await request(server)
|
|
102
|
+
.get('/test.txt')
|
|
103
|
+
.expect(200);
|
|
104
|
+
|
|
105
|
+
expect(fileResponse.text).toContain('hello world');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('Regression: Sort links still work', () => {
|
|
110
|
+
test('Sort header links should work', async () => {
|
|
111
|
+
const response = await request(server)
|
|
112
|
+
.get('/')
|
|
113
|
+
.expect(200);
|
|
114
|
+
|
|
115
|
+
// Sort links SHOULD have query parameters
|
|
116
|
+
expect(response.text).toContain('?sort=name');
|
|
117
|
+
expect(response.text).toContain('?sort=type');
|
|
118
|
+
expect(response.text).toContain('?sort=size');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('Sort indicators should toggle', async () => {
|
|
122
|
+
const response1 = await request(server)
|
|
123
|
+
.get('/?sort=name&order=asc')
|
|
124
|
+
.expect(200);
|
|
125
|
+
|
|
126
|
+
expect(response1.text).toContain('↑');
|
|
127
|
+
|
|
128
|
+
const response2 = await request(server)
|
|
129
|
+
.get('/?sort=name&order=desc')
|
|
130
|
+
.expect(200);
|
|
131
|
+
|
|
132
|
+
expect(response2.text).toContain('↓');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,299 @@
|
|
|
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
|
+
showDirContents: 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
|
+
});
|
|
@@ -17,15 +17,84 @@ const koaClassicServer = require('../index.cjs');
|
|
|
17
17
|
const path = require('path');
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
|
|
20
|
-
const BENCHMARK_DIR = path.join(__dirname, '
|
|
20
|
+
const BENCHMARK_DIR = path.join(__dirname, 'benchmark-data');
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
// Auto-setup benchmark data if not exists
|
|
23
|
+
function setupBenchmarkData() {
|
|
24
|
+
if (fs.existsSync(BENCHMARK_DIR)) {
|
|
25
|
+
return; // Data already exists
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log('\n🔧 Setting up benchmark data automatically...\n');
|
|
29
|
+
|
|
30
|
+
// Create benchmark directory
|
|
31
|
+
fs.mkdirSync(BENCHMARK_DIR, { recursive: true });
|
|
32
|
+
|
|
33
|
+
// Create small files (1KB each)
|
|
34
|
+
console.log(' Creating 100 small files (1KB each)...');
|
|
35
|
+
const smallDir = path.join(BENCHMARK_DIR, 'small-files');
|
|
36
|
+
fs.mkdirSync(smallDir);
|
|
37
|
+
for (let i = 1; i <= 100; i++) {
|
|
38
|
+
const content = 'X'.repeat(1024);
|
|
39
|
+
fs.writeFileSync(path.join(smallDir, `file-${i}.txt`), content);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Create medium files (100KB each)
|
|
43
|
+
console.log(' Creating 50 medium files (100KB each)...');
|
|
44
|
+
const mediumDir = path.join(BENCHMARK_DIR, 'medium-files');
|
|
45
|
+
fs.mkdirSync(mediumDir);
|
|
46
|
+
for (let i = 1; i <= 50; i++) {
|
|
47
|
+
const content = 'X'.repeat(100 * 1024);
|
|
48
|
+
fs.writeFileSync(path.join(mediumDir, `file-${i}.txt`), content);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Create large files (1MB each)
|
|
52
|
+
console.log(' Creating 10 large files (1MB each)...');
|
|
53
|
+
const largeDir = path.join(BENCHMARK_DIR, 'large-files');
|
|
54
|
+
fs.mkdirSync(largeDir);
|
|
55
|
+
for (let i = 1; i <= 10; i++) {
|
|
56
|
+
const content = 'X'.repeat(1024 * 1024);
|
|
57
|
+
fs.writeFileSync(path.join(largeDir, `file-${i}.txt`), content);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create directory with 1000 files
|
|
61
|
+
console.log(' Creating directory with 1000 files...');
|
|
62
|
+
const largeDirListing = path.join(BENCHMARK_DIR, 'large-directory');
|
|
63
|
+
fs.mkdirSync(largeDirListing);
|
|
64
|
+
for (let i = 1; i <= 1000; i++) {
|
|
65
|
+
const content = `File number ${i}\n`;
|
|
66
|
+
fs.writeFileSync(path.join(largeDirListing, `item-${String(i).padStart(4, '0')}.txt`), content);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Create directory with 10,000 files
|
|
70
|
+
console.log(' Creating directory with 10,000 files (this may take a moment)...');
|
|
71
|
+
const veryLargeDirListing = path.join(BENCHMARK_DIR, 'very-large-directory');
|
|
72
|
+
fs.mkdirSync(veryLargeDirListing);
|
|
73
|
+
for (let i = 1; i <= 10000; i++) {
|
|
74
|
+
const content = `File number ${i}\n`;
|
|
75
|
+
fs.writeFileSync(path.join(veryLargeDirListing, `item-${String(i).padStart(5, '0')}.txt`), content);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Create HTML file for caching test
|
|
79
|
+
const htmlContent = `<!DOCTYPE html>
|
|
80
|
+
<html>
|
|
81
|
+
<head>
|
|
82
|
+
<meta charset="UTF-8">
|
|
83
|
+
<title>Benchmark Test</title>
|
|
84
|
+
</head>
|
|
85
|
+
<body>
|
|
86
|
+
<h1>Benchmark Test Page</h1>
|
|
87
|
+
<p>${'Lorem ipsum dolor sit amet. '.repeat(100)}</p>
|
|
88
|
+
</body>
|
|
89
|
+
</html>`;
|
|
90
|
+
fs.writeFileSync(path.join(BENCHMARK_DIR, 'test.html'), htmlContent);
|
|
91
|
+
|
|
92
|
+
console.log('\n✅ Benchmark data setup complete!\n');
|
|
27
93
|
}
|
|
28
94
|
|
|
95
|
+
// Setup benchmark data automatically if needed
|
|
96
|
+
setupBenchmarkData();
|
|
97
|
+
|
|
29
98
|
// Helper to measure execution time
|
|
30
99
|
function measureTime(fn) {
|
|
31
100
|
const start = process.hrtime.bigint();
|