koa-classic-server 2.6.1 → 3.0.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/CLAUDE.md +101 -0
- package/README.md +564 -591
- package/__tests__/benchmark-results-v3.0.0.txt +372 -0
- package/__tests__/benchmark.js +1 -1
- package/__tests__/caching-headers.test.js +30 -30
- package/__tests__/compression-fixtures/data.json +1 -0
- package/__tests__/compression-fixtures/large.txt +1 -0
- package/__tests__/compression-fixtures/small.txt +1 -0
- package/__tests__/compression.test.js +284 -0
- package/__tests__/customTest/serversToLoad.util.js +5 -5
- package/__tests__/demo-regex-index.js +4 -4
- package/__tests__/deprecation-warnings.test.js +71 -183
- package/__tests__/directory-sorting-links.test.js +1 -1
- package/__tests__/dt-unknown.test.js +39 -28
- package/__tests__/ejs.test.js +1 -1
- package/__tests__/hidden-fixtures/.dot-dir/inside.txt +1 -0
- package/__tests__/hidden-fixtures/.env +2 -0
- package/__tests__/hidden-fixtures/.well-known/acme-challenge.txt +1 -0
- package/__tests__/hidden-fixtures/config/secrets/password.txt +1 -0
- package/__tests__/hidden-fixtures/data.key +1 -0
- package/__tests__/hidden-fixtures/file.secret +1 -0
- package/__tests__/hidden-fixtures/index.html +1 -0
- package/__tests__/hidden-fixtures/normal.txt +1 -0
- package/__tests__/hidden-fixtures/subdir/.env +1 -0
- package/__tests__/hidden-fixtures/subdir/regular.txt +1 -0
- package/__tests__/hidden-option.test.js +407 -0
- package/__tests__/hideExtension.test.js +70 -13
- package/__tests__/index-option.test.js +18 -16
- package/__tests__/index.test.js +14 -10
- package/__tests__/listing.test.js +437 -0
- package/__tests__/logger.test.js +232 -0
- package/__tests__/range-fixtures/sample.txt +1 -0
- package/__tests__/range.test.js +223 -0
- package/__tests__/security-headers.test.js +165 -0
- package/__tests__/security.test.js +148 -162
- package/__tests__/server-cache-fixtures/large.txt +1 -0
- package/__tests__/server-cache-fixtures/small.txt +1 -0
- package/__tests__/server-cache.test.js +594 -0
- package/__tests__/symlink.test.js +18 -15
- package/__tests__/template-timeout.test.js +321 -0
- package/docs/ACTION_PLAN.md +293 -0
- package/docs/CHANGELOG.md +289 -0
- package/docs/CODE_REVIEW.md +2 -0
- package/docs/DOCUMENTATION.md +259 -32
- package/docs/EXAMPLES_INDEX_OPTION.md +3 -3
- package/docs/FLOW_DIAGRAM.md +15 -13
- package/docs/INDEX_OPTION_PRIORITY.md +2 -2
- package/docs/OPTIMIZATION_HTTP_CACHING.md +2 -0
- package/docs/OPTIMIZATION_ROADMAP_for_V3.md +864 -0
- package/docs/PERFORMANCE_COMPARISON.md +7 -7
- package/docs/security_improvement_for_V3.md +421 -0
- package/docs/template-engine/TEMPLATE_ENGINE_GUIDE.md +5 -5
- package/docs/template-engine/esempi-incrementali.js +1 -1
- package/eslint.config.mjs +17 -0
- package/index.cjs +1507 -429
- package/index.mjs +1 -5
- package/package.json +9 -1
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
const supertest = require('supertest');
|
|
2
|
+
const koaClassicServer = require('../index.cjs');
|
|
3
|
+
const Koa = require('koa');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const root = path.join(__dirname, 'range-fixtures');
|
|
7
|
+
|
|
8
|
+
// Fixture: sample.txt contains '0123456789abcdefghij' — exactly 20 bytes, no newline
|
|
9
|
+
const FILE_SIZE = 20;
|
|
10
|
+
const FILE_CONTENT = '0123456789abcdefghij';
|
|
11
|
+
|
|
12
|
+
function createApp(opts = {}) {
|
|
13
|
+
const app = new Koa();
|
|
14
|
+
app.use(koaClassicServer(root, { dirListing: { enabled: false }, ...opts }));
|
|
15
|
+
return app.listen();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ─── Accept-Ranges advertisement ─────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
describe('Range — Accept-Ranges header', () => {
|
|
21
|
+
let server;
|
|
22
|
+
beforeAll(() => { server = createApp(); });
|
|
23
|
+
afterAll(() => server.close());
|
|
24
|
+
|
|
25
|
+
test('Accept-Ranges: bytes present on 200 response', async () => {
|
|
26
|
+
const res = await supertest(server).get('/sample.txt');
|
|
27
|
+
expect(res.status).toBe(200);
|
|
28
|
+
expect(res.headers['accept-ranges']).toBe('bytes');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('Accept-Ranges not added to directory listings', async () => {
|
|
32
|
+
const listServer = createApp({ dirListing: { enabled: true } });
|
|
33
|
+
const res = await supertest(listServer).get('/');
|
|
34
|
+
listServer.close();
|
|
35
|
+
expect(res.headers['accept-ranges']).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ─── Basic 206 responses ──────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
describe('Range — basic 206 Partial Content', () => {
|
|
42
|
+
let server;
|
|
43
|
+
beforeAll(() => { server = createApp(); });
|
|
44
|
+
afterAll(() => server.close());
|
|
45
|
+
|
|
46
|
+
test('bytes=0-4 → first 5 bytes', async () => {
|
|
47
|
+
const res = await supertest(server).get('/sample.txt').set('Range', 'bytes=0-4');
|
|
48
|
+
expect(res.status).toBe(206);
|
|
49
|
+
expect(res.headers['content-range']).toBe(`bytes 0-4/${FILE_SIZE}`);
|
|
50
|
+
expect(res.headers['content-length']).toBe('5');
|
|
51
|
+
expect(res.text).toBe('01234');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('bytes=10- → open range from offset 10', async () => {
|
|
55
|
+
const res = await supertest(server).get('/sample.txt').set('Range', 'bytes=10-');
|
|
56
|
+
expect(res.status).toBe(206);
|
|
57
|
+
expect(res.headers['content-range']).toBe(`bytes 10-19/${FILE_SIZE}`);
|
|
58
|
+
expect(res.headers['content-length']).toBe('10');
|
|
59
|
+
expect(res.text).toBe('abcdefghij');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('bytes=-5 → last 5 bytes', async () => {
|
|
63
|
+
const res = await supertest(server).get('/sample.txt').set('Range', 'bytes=-5');
|
|
64
|
+
expect(res.status).toBe(206);
|
|
65
|
+
expect(res.headers['content-range']).toBe(`bytes 15-19/${FILE_SIZE}`);
|
|
66
|
+
expect(res.headers['content-length']).toBe('5');
|
|
67
|
+
expect(res.text).toBe('fghij');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('bytes=0-19 (full file) → 206', async () => {
|
|
71
|
+
const res = await supertest(server).get('/sample.txt').set('Range', 'bytes=0-19');
|
|
72
|
+
expect(res.status).toBe(206);
|
|
73
|
+
expect(res.headers['content-range']).toBe(`bytes 0-19/${FILE_SIZE}`);
|
|
74
|
+
expect(res.headers['content-length']).toBe('20');
|
|
75
|
+
expect(res.text).toBe(FILE_CONTENT);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('bytes=-999 (suffix > file size) → 206 full file', async () => {
|
|
79
|
+
const res = await supertest(server).get('/sample.txt').set('Range', 'bytes=-999');
|
|
80
|
+
expect(res.status).toBe(206);
|
|
81
|
+
expect(res.headers['content-range']).toBe(`bytes 0-19/${FILE_SIZE}`);
|
|
82
|
+
expect(res.headers['content-length']).toBe('20');
|
|
83
|
+
expect(res.text).toBe(FILE_CONTENT);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('bytes=5-999 (end > file size) → end clamped to 19', async () => {
|
|
87
|
+
const res = await supertest(server).get('/sample.txt').set('Range', 'bytes=5-999');
|
|
88
|
+
expect(res.status).toBe(206);
|
|
89
|
+
expect(res.headers['content-range']).toBe(`bytes 5-19/${FILE_SIZE}`);
|
|
90
|
+
expect(res.headers['content-length']).toBe('15');
|
|
91
|
+
expect(res.text).toBe('56789abcdefghij');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('Accept-Ranges present on 206 response', async () => {
|
|
95
|
+
const res = await supertest(server).get('/sample.txt').set('Range', 'bytes=0-4');
|
|
96
|
+
expect(res.headers['accept-ranges']).toBe('bytes');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('single byte range bytes=7-7', async () => {
|
|
100
|
+
const res = await supertest(server).get('/sample.txt').set('Range', 'bytes=7-7');
|
|
101
|
+
expect(res.status).toBe(206);
|
|
102
|
+
expect(res.headers['content-range']).toBe(`bytes 7-7/${FILE_SIZE}`);
|
|
103
|
+
expect(res.headers['content-length']).toBe('1');
|
|
104
|
+
expect(res.text).toBe('7');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ─── 416 Range Not Satisfiable ────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
describe('Range — 416 Range Not Satisfiable', () => {
|
|
111
|
+
let server;
|
|
112
|
+
beforeAll(() => { server = createApp(); });
|
|
113
|
+
afterAll(() => server.close());
|
|
114
|
+
|
|
115
|
+
test('start beyond file size → 416', async () => {
|
|
116
|
+
const res = await supertest(server).get('/sample.txt').set('Range', 'bytes=100-200');
|
|
117
|
+
expect(res.status).toBe(416);
|
|
118
|
+
expect(res.headers['content-range']).toBe(`bytes */${FILE_SIZE}`);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('start exactly at file size → 416', async () => {
|
|
122
|
+
const res = await supertest(server).get('/sample.txt').set('Range', `bytes=${FILE_SIZE}-`);
|
|
123
|
+
expect(res.status).toBe(416);
|
|
124
|
+
expect(res.headers['content-range']).toBe(`bytes */${FILE_SIZE}`);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ─── Fallback to 200 ──────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
describe('Range — fallback to 200 on invalid Range', () => {
|
|
131
|
+
let server;
|
|
132
|
+
beforeAll(() => { server = createApp(); });
|
|
133
|
+
afterAll(() => server.close());
|
|
134
|
+
|
|
135
|
+
test('malformed Range header → 200 full file', async () => {
|
|
136
|
+
const res = await supertest(server).get('/sample.txt').set('Range', 'bytes=abc');
|
|
137
|
+
expect(res.status).toBe(200);
|
|
138
|
+
expect(res.text).toBe(FILE_CONTENT);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('multi-range → 200 full file', async () => {
|
|
142
|
+
const res = await supertest(server).get('/sample.txt').set('Range', 'bytes=0-2,5-7');
|
|
143
|
+
expect(res.status).toBe(200);
|
|
144
|
+
expect(res.text).toBe(FILE_CONTENT);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('non-bytes unit → 200 full file', async () => {
|
|
148
|
+
const res = await supertest(server).get('/sample.txt').set('Range', 'kilobytes=0-100');
|
|
149
|
+
expect(res.status).toBe(200);
|
|
150
|
+
expect(res.text).toBe(FILE_CONTENT);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('inverted range bytes=10-5 → 200 full file', async () => {
|
|
154
|
+
const res = await supertest(server).get('/sample.txt').set('Range', 'bytes=10-5');
|
|
155
|
+
expect(res.status).toBe(200);
|
|
156
|
+
expect(res.text).toBe(FILE_CONTENT);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ─── If-Range ────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
describe('Range — If-Range', () => {
|
|
163
|
+
let server;
|
|
164
|
+
let currentEtag;
|
|
165
|
+
|
|
166
|
+
beforeAll(async () => {
|
|
167
|
+
server = createApp({ browserCacheEnabled: true });
|
|
168
|
+
const res = await supertest(server).get('/sample.txt');
|
|
169
|
+
currentEtag = res.headers['etag'];
|
|
170
|
+
});
|
|
171
|
+
afterAll(() => server.close());
|
|
172
|
+
|
|
173
|
+
test('If-Range with matching ETag → 206', async () => {
|
|
174
|
+
const res = await supertest(server)
|
|
175
|
+
.get('/sample.txt')
|
|
176
|
+
.set('Range', 'bytes=0-4')
|
|
177
|
+
.set('If-Range', currentEtag);
|
|
178
|
+
expect(res.status).toBe(206);
|
|
179
|
+
expect(res.text).toBe('01234');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('If-Range with wrong ETag → 200 full file', async () => {
|
|
183
|
+
const res = await supertest(server)
|
|
184
|
+
.get('/sample.txt')
|
|
185
|
+
.set('Range', 'bytes=0-4')
|
|
186
|
+
.set('If-Range', '"wrong-etag-does-not-match"');
|
|
187
|
+
expect(res.status).toBe(200);
|
|
188
|
+
expect(res.text).toBe(FILE_CONTENT);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ─── HEAD + Range ─────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
describe('Range — HEAD method', () => {
|
|
195
|
+
let server;
|
|
196
|
+
beforeAll(() => { server = createApp({ method: ['GET', 'HEAD'] }); });
|
|
197
|
+
afterAll(() => server.close());
|
|
198
|
+
|
|
199
|
+
test('HEAD + Range → 206 status and correct headers, no body', async () => {
|
|
200
|
+
const res = await supertest(server)
|
|
201
|
+
.head('/sample.txt')
|
|
202
|
+
.set('Range', 'bytes=0-4');
|
|
203
|
+
expect(res.status).toBe(206);
|
|
204
|
+
expect(res.headers['content-range']).toBe(`bytes 0-4/${FILE_SIZE}`);
|
|
205
|
+
expect(res.headers['content-length']).toBe('5');
|
|
206
|
+
// HEAD response: no body sent (supertest reports undefined, not '')
|
|
207
|
+
expect(res.body).toEqual({});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('HEAD without Range → 200, Accept-Ranges present', async () => {
|
|
211
|
+
const res = await supertest(server).head('/sample.txt');
|
|
212
|
+
expect(res.status).toBe(200);
|
|
213
|
+
expect(res.headers['accept-ranges']).toBe('bytes');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('HEAD + out-of-bounds Range → 416', async () => {
|
|
217
|
+
const res = await supertest(server)
|
|
218
|
+
.head('/sample.txt')
|
|
219
|
+
.set('Range', 'bytes=999-');
|
|
220
|
+
expect(res.status).toBe(416);
|
|
221
|
+
expect(res.headers['content-range']).toBe(`bytes */${FILE_SIZE}`);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const supertest = require('supertest');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
const koaClassicServer = require('../index.cjs');
|
|
4
|
+
const Koa = require('koa');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const root = path.join(__dirname, 'publicWwwTest');
|
|
8
|
+
|
|
9
|
+
// Compute the expected CSP hash from the actual inline CSS in the response, so
|
|
10
|
+
// CSS edits to the listing template do not require updating a hardcoded hash here.
|
|
11
|
+
async function expectedListingCsp(server, requestPath = '/') {
|
|
12
|
+
const res = await supertest(server).get(requestPath);
|
|
13
|
+
const m = res.text.match(/<style>([\s\S]*?)<\/style>/);
|
|
14
|
+
if (!m) throw new Error('No <style> block in listing HTML — cannot compute expected CSP');
|
|
15
|
+
const cssHash = 'sha256-' + crypto.createHash('sha256').update(m[1], 'utf8').digest('base64');
|
|
16
|
+
return `default-src 'none'; style-src '${cssHash}'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const NOT_FOUND_CSP = "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'";
|
|
20
|
+
|
|
21
|
+
const COMMON_HEADERS = {
|
|
22
|
+
'x-content-type-options': 'nosniff',
|
|
23
|
+
'x-frame-options': 'DENY',
|
|
24
|
+
'referrer-policy': 'no-referrer',
|
|
25
|
+
'permissions-policy': 'camera=(), microphone=(), geolocation=(), payment=()',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function createApp(opts = {}) {
|
|
29
|
+
const app = new Koa();
|
|
30
|
+
app.use(koaClassicServer(root, { dirListing: { enabled: true }, ...opts }));
|
|
31
|
+
return app.listen();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Directory listing ────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
describe('Security headers — directory listing page', () => {
|
|
37
|
+
let server;
|
|
38
|
+
beforeAll(() => { server = createApp(); });
|
|
39
|
+
afterAll(() => server.close());
|
|
40
|
+
|
|
41
|
+
test('Content-Security-Policy uses hash-based style-src', async () => {
|
|
42
|
+
const res = await supertest(server).get('/');
|
|
43
|
+
expect(res.headers['content-security-policy']).toBe(await expectedListingCsp(server));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('X-Content-Type-Options: nosniff', async () => {
|
|
47
|
+
const res = await supertest(server).get('/');
|
|
48
|
+
expect(res.headers['x-content-type-options']).toBe('nosniff');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('X-Frame-Options: DENY', async () => {
|
|
52
|
+
const res = await supertest(server).get('/');
|
|
53
|
+
expect(res.headers['x-frame-options']).toBe('DENY');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('Referrer-Policy: no-referrer', async () => {
|
|
57
|
+
const res = await supertest(server).get('/');
|
|
58
|
+
expect(res.headers['referrer-policy']).toBe('no-referrer');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('Permissions-Policy disables camera, microphone, geolocation, payment', async () => {
|
|
62
|
+
const res = await supertest(server).get('/');
|
|
63
|
+
expect(res.headers['permissions-policy']).toBe('camera=(), microphone=(), geolocation=(), payment=()');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('All common security headers present on listing', async () => {
|
|
67
|
+
const res = await supertest(server).get('/');
|
|
68
|
+
for (const [header, value] of Object.entries(COMMON_HEADERS)) {
|
|
69
|
+
expect(res.headers[header]).toBe(value);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('CSP style-src hash in listing matches actual inline CSS', async () => {
|
|
74
|
+
const res = await supertest(server).get('/');
|
|
75
|
+
const styleMatch = res.text.match(/<style>([\s\S]*?)<\/style>/);
|
|
76
|
+
expect(styleMatch).toBeTruthy();
|
|
77
|
+
const expectedHash = 'sha256-' + crypto.createHash('sha256').update(styleMatch[1], 'utf8').digest('base64');
|
|
78
|
+
expect(res.headers['content-security-policy']).toContain(expectedHash);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ─── 404 Not Found page ───────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
describe('Security headers — 404 Not Found page', () => {
|
|
85
|
+
let server;
|
|
86
|
+
beforeAll(() => { server = createApp(); });
|
|
87
|
+
afterAll(() => server.close());
|
|
88
|
+
|
|
89
|
+
test('Content-Security-Policy on 404 is fully restrictive (no style-src)', async () => {
|
|
90
|
+
const res = await supertest(server).get('/nonexistent-file-xyz.txt');
|
|
91
|
+
expect(res.status).toBe(404);
|
|
92
|
+
expect(res.headers['content-security-policy']).toBe(NOT_FOUND_CSP);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('All common security headers present on 404', async () => {
|
|
96
|
+
const res = await supertest(server).get('/nonexistent-file-xyz.txt');
|
|
97
|
+
for (const [header, value] of Object.entries(COMMON_HEADERS)) {
|
|
98
|
+
expect(res.headers[header]).toBe(value);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('CSP on 404 does NOT contain style-src', async () => {
|
|
103
|
+
const res = await supertest(server).get('/nonexistent-file-xyz.txt');
|
|
104
|
+
expect(res.headers['content-security-policy']).not.toContain('style-src');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ─── Directory listing disabled (dirListing: { enabled: false }) ─────────────────────
|
|
109
|
+
|
|
110
|
+
describe('Security headers — 404 when directory listing disabled', () => {
|
|
111
|
+
let server;
|
|
112
|
+
beforeAll(() => { server = createApp({ dirListing: { enabled: false } }); });
|
|
113
|
+
afterAll(() => server.close());
|
|
114
|
+
|
|
115
|
+
test('Security headers present when directory listing disabled', async () => {
|
|
116
|
+
const res = await supertest(server).get('/');
|
|
117
|
+
expect(res.status).toBe(404);
|
|
118
|
+
expect(res.headers['content-security-policy']).toBe(NOT_FOUND_CSP);
|
|
119
|
+
for (const [header, value] of Object.entries(COMMON_HEADERS)) {
|
|
120
|
+
expect(res.headers[header]).toBe(value);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ─── Served user files — NO security headers ─────────────────────────────────
|
|
126
|
+
|
|
127
|
+
describe('Security headers — NOT added to user-served files', () => {
|
|
128
|
+
let server;
|
|
129
|
+
beforeAll(() => { server = createApp(); });
|
|
130
|
+
afterAll(() => server.close());
|
|
131
|
+
|
|
132
|
+
test('Served static file has no Content-Security-Policy', async () => {
|
|
133
|
+
const res = await supertest(server).get('/test.txt');
|
|
134
|
+
expect(res.status).toBe(200);
|
|
135
|
+
expect(res.headers['content-security-policy']).toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('Served static file has no X-Frame-Options', async () => {
|
|
139
|
+
const res = await supertest(server).get('/test.txt');
|
|
140
|
+
expect(res.headers['x-frame-options']).toBeUndefined();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('Served HTML file has no Content-Security-Policy (user file)', async () => {
|
|
144
|
+
const res = await supertest(server).get('/test-page.html');
|
|
145
|
+
expect(res.status).toBe(200);
|
|
146
|
+
expect(res.headers['content-security-policy']).toBeUndefined();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ─── Subdirectory listing ─────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
describe('Security headers — subdirectory listing page', () => {
|
|
153
|
+
let server;
|
|
154
|
+
beforeAll(() => { server = createApp(); });
|
|
155
|
+
afterAll(() => server.close());
|
|
156
|
+
|
|
157
|
+
test('Listing of subdirectory also has security headers', async () => {
|
|
158
|
+
const res = await supertest(server).get('/cartella/');
|
|
159
|
+
expect(res.status).toBe(200);
|
|
160
|
+
expect(res.headers['content-security-policy']).toBe(await expectedListingCsp(server, '/cartella/'));
|
|
161
|
+
for (const [header, value] of Object.entries(COMMON_HEADERS)) {
|
|
162
|
+
expect(res.headers[header]).toBe(value);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
});
|