UNPKG

als-send-file

Version:

file serving with advanced options for caching, headers, and error handling, compatible with Express middleware.

193 lines (164 loc) 8.59 kB
const { describe, it, before, after, beforeEach } = require('node:test'); const assert = require('node:assert'); const http = require('http'); const port = 1234; const baseUrl = `http://localhost:${port}/`; const fileHandler = require('../lib/file-handler.js'); const httpErrHandler = (res, code, msg) => { res.writeHead(code); res.end(msg); }; const { join } = require('path'); const root = join(__dirname, '..'); const { writeFileSync, unlinkSync, existsSync } = require('node:fs'); const server = http.createServer(async (req, res) => { const { search, pathname } = new URL(req.url, 'http://some.com'); const options = {}; search.replace('?', '').split(';').forEach(val => { let [k, v] = val.split('='); if (v === 'true') v = true if (k) options[k] = v; }); if (pathname === '/early-end') setTimeout(() => { res.end() }, 1); await fileHandler(req, res, join(root, pathname), options, httpErrHandler); }); async function request(url, headers = {}, method = 'GET') { const response = await fetch(`${baseUrl}${url}`, { method, headers }); const text = await response.text(); return [response.status, text, response.headers, response]; } describe('fileHandler Tests', () => { before(() => { server.listen(port, () => { console.log(`Server running on http://localhost:${port}`); }); }); after((t,done) => { server.close(done); }); describe('try catch for stat(filepath)', () => { it('should return 404 if file not found', async () => { const [status, text] = await request('nonexistent-file.txt'); assert.strictEqual(status, 404); assert.strictEqual(text, 'File nonexistent-file.txt not found'); }); }) describe('etag and cache control tests', () => { it('should handle ETag and return 304 if not modified', async () => { const testFilePath = 'test-file-etag.txt'; writeFileSync(testFilePath, 'Hello from test-file-etag.txt'); const [status, , headers] = await request(testFilePath + '?etag=true'); const etag = headers.get('etag'); const [statusNotModified] = await request(testFilePath + '?etag=true', { 'If-None-Match': etag }); assert.strictEqual(statusNotModified, 304); unlinkSync(testFilePath); }); it('should handle Cache-Control headers', async () => { const testFilePath = 'test-file-cache.txt'; writeFileSync(testFilePath, 'Hello from test-file-cache.txt'); const [status, , headers] = await request(`${testFilePath}?maxAge=3600;noCache=true;public=public`); assert.strictEqual(status, 200); assert.strictEqual(headers.get('cache-control'), 'public, max-age=3600, no-cache'); unlinkSync(testFilePath); }); }) describe('Content-Disposition tests', () => { const testFilePath = 'test-file-download.txt'; before(() => { writeFileSync(testFilePath, 'Hello from test-file-download.txt'); }) after(() => { unlinkSync(testFilePath); }) it('should handle Content-Disposition for download', async () => { const [status, , headers] = await request(`${testFilePath}?download=true`); assert.strictEqual(status, 200); assert.strictEqual(headers.get('content-disposition'), `attachment; filename="${testFilePath}"`); }); it('should handle Content-Disposition for inline', async () => { const [status, , headers] = await request(`${testFilePath}`); assert.strictEqual(status, 200); assert.strictEqual(headers.get('content-disposition'), `inline; filename="${testFilePath}"`); }); }) describe('charset and mime tests', () => { const testFilePath = 'test-file.txt', content = 'Hello from test-file.txt'; before(() => { writeFileSync(testFilePath, content); }) after(() => { if(existsSync(testFilePath)) unlinkSync(testFilePath); }) it('should handle charset in Content-Type header', async () => { const [status, , headers] = await request(`${testFilePath}?charset=utf-8`); assert(status === 200); assert(headers.get('content-type') === 'text/plain; charset=utf-8'); }); it('should serve a file successfully', async () => { const [status, text, headers] = await request(testFilePath); assert(status === 200); assert(text.includes(content)); assert(headers.get('content-type') === 'text/plain'); }); it('should serve a file successfully', async () => { const testFilePath = 'test-file', content = 'Hello from test-file'; writeFileSync(testFilePath, content); const [status, text, headers] = await request(testFilePath); assert(status === 200); assert(text.includes(content)); assert(headers.get('content-type') === 'text/plain'); unlinkSync(testFilePath) }); it('should handle all Cache-Control directives together', async () => { const testFilePath = 'test-file-combo-cache.txt'; writeFileSync(testFilePath, 'Hello from test-file-combo-cache.txt'); const [status, , headers] = await request(`${testFilePath}?maxAge=3600;noCache=true;noStore=true;public=public`); assert.strictEqual(status, 200); assert.strictEqual(headers.get('cache-control'), 'public, max-age=3600, no-cache, no-store'); unlinkSync(testFilePath); }); it('should handle all Content-Disposition and charset options', async () => { const testFilePath = 'test-file-combo-disposition.txt'; writeFileSync(testFilePath, 'Hello from test-file-combo-disposition.txt'); const [status, , headers] = await request(`${testFilePath}?download=true;charset=utf-8`); assert.strictEqual(status, 200); assert.strictEqual(headers.get('content-disposition'), `attachment; filename="${testFilePath}"`); assert.strictEqual(headers.get('content-type'), 'text/plain; charset=utf-8'); unlinkSync(testFilePath); }); }) describe('sendFile tests', () => { it('should return 416 for invalid range requests', async () => { const testFilePath = 'test-file-invalid-range.txt'; writeFileSync(testFilePath, 'Hello from test-file-invalid-range.txt'); const [status, text] = await request(testFilePath, { 'Range': 'bytes=-' }); assert.strictEqual(status, 416); assert.strictEqual(text, 'Range is not valid'); unlinkSync(testFilePath); }); it('should handle range requests correctly', async () => { const testFilePath = 'test-file-range.txt'; writeFileSync(testFilePath, 'Hello from test-file-range.txt'); const [status, text, headers] = await request(testFilePath, { 'Range': 'bytes=0-4' }); assert.strictEqual(status, 206); assert.strictEqual(text, 'Hello'); assert.strictEqual(headers.get('content-range'), `bytes 0-4/30`); unlinkSync(testFilePath); }); }) describe('Errors', () => { it('should abort connection', async () => { const testFilePath = 'test-file-abort.txt'; writeFileSync(testFilePath, 'Hello from test-file-abort.txt'); const controller = new AbortController(); const signal = controller.signal; // Отправка запроса с использованием AbortController const fetchPromise = fetch(`${baseUrl}${testFilePath}`, { signal }) .then(response => response.text()) .catch(err => { if (err.name === 'AbortError') return 'aborted'; // throw err; }); setTimeout(() => { controller.abort() }, 1); const result = await fetchPromise; assert.strictEqual(result, 'aborted'); unlinkSync(testFilePath); }); it('should end response before file is fully sent', async () => { const testFilePath = 'test-file-early-end.txt'; writeFileSync(testFilePath, 'Hello from test-file-early-end.txt'); // Отправка запроса к специальному маршруту, который завершает res.end раньше const [status, text] = await request(`early-end`); assert.strictEqual(status, 200); assert.strictEqual(text, ''); // Ожидаем, что текст будет пустым, так как res.end вызвано до завершения передачи файла unlinkSync(testFilePath); }); }); });