'use strict'; require('../tools/js/v3-models.js'); const assert = require('assert'); global.FormData = require('form-data'); const TestServer = require('./resources/test-server.js'); const TemporaryFile = require('./resources/temporary-file.js').TemporaryFile; const prepareServerTest = require('./resources/common-operations.js').prepareServerTest; describe('/api/uploaded-file', function () { prepareServerTest(this); TemporaryFile.inject(); it('should return "InvalidArguments" when neither path nor sha256 query is set', () => { return TestServer.remoteAPI().getJSON('/api/uploaded-file').then((content) => { assert.strictEqual(content['status'], 'InvalidArguments'); return TestServer.remoteAPI().getJSON('/api/uploaded-file/'); }).then((content) => { assert.strictEqual(content['status'], 'InvalidArguments'); }); }); it('should return 404 when there is no file with the specified ID', () => { return TestServer.remoteAPI().getJSON('/api/uploaded-file/1').then((content) => { assert(false, 'should never be reached'); }, (error) => { assert.strictEqual(error, 404); }); }); it('should return 404 when the specified ID is not a valid integer', () => { return TestServer.remoteAPI().getJSON('/api/uploaded-file/foo').then((content) => { assert(false, 'should never be reached'); }, (error) => { assert.strictEqual(error, 404); }); }); it('should return the file content matching the specified file ID', () => { let uploadedFile; return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => { return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true}); }).then((response) => { uploadedFile = response['uploadedFile']; return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${uploadedFile['id']}`, 'GET', null, null); }).then((response) => { assert.strictEqual(response.responseText, 'some content'); }); }); it('should return the file content with createdAt using POSIX timestamp in UTC', () => { let uploadedFile; const startTime = +Date.now(); return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => { return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true}); }).then((response) => { uploadedFile = response['uploadedFile']; console.assert(typeof(uploadedFile.createdAt) == 'number') const createdAt = +new Date(uploadedFile.createdAt); const endTime = +Date.now(); assert(startTime <= createdAt, 'createdAt should be after the time POST request was made'); assert(createdAt <= endTime, 'createdAt should be before the uploadedFile response had finished'); }); }); it('should return "NotFound" when the specified SHA256 is invalid', () => { return TestServer.remoteAPI().getJSON('/api/uploaded-file/?sha256=abc').then((content) => { assert.strictEqual(content['status'], 'NotFound'); }); }); it('should return "NotFound" when there is no file matching the specified SHA256 ', () => { return TestServer.remoteAPI().getJSON('/api/uploaded-file/?sha256=5256ec18f11624025905d057d6befb03d77b243511ac5f77ed5e0221ce6d84b5').then((content) => { assert.strictEqual(content['status'], 'NotFound'); }); }); it('should return the meta data of the file with the specified SHA256', () => { let uploadedFile; return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => { return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true}); }).then((response) => { uploadedFile = response['uploadedFile']; return TestServer.remoteAPI().getJSON(`/api/uploaded-file/?sha256=${uploadedFile['sha256']}`); }).then((response) => { assert.deepStrictEqual(uploadedFile, response['uploadedFile']); }); }); it('should return "NotFound" when the file matching the specified SHA256 had already been deleted', () => { let uploadedFile; return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => { return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true}); }).then((response) => { uploadedFile = response['uploadedFile']; const db = TestServer.database(); return db.query(`UPDATE uploaded_files SET file_deleted_at = now() at time zone 'utc'`); }).then(() => { return TestServer.remoteAPI().getJSON(`/api/uploaded-file/?sha256=${uploadedFile['sha256']}`); }).then((content) => { assert.strictEqual(content['status'], 'NotFound'); }); }); it('should respond with ETag, Acccept-Ranges, Content-Disposition, Content-Length, and Last-Modified headers', () => { let uploadedFile; return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => { return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true}); }).then((response) => { uploadedFile = response['uploadedFile']; return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${uploadedFile['id']}`, 'GET', null, null); }).then((response) => { const headers = response.headers; assert(Object.keys(headers).includes('etag')); assert.strictEqual(headers['etag'], uploadedFile['sha256']); assert(Object.keys(headers).includes('accept-ranges')); assert.strictEqual(headers['accept-ranges'], 'bytes'); assert(Object.keys(headers).includes('content-disposition')); assert.strictEqual(headers['content-disposition'], `attachment; filename*=utf-8''some.dat`); assert(Object.keys(headers).includes('content-length')); assert.strictEqual(headers['content-length'], uploadedFile['size']); assert(Object.keys(headers).includes('last-modified')); }); }); it('should respond with the same Last-Modified each time', () => { let id; let lastModified; return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => { return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true}); }).then((response) => { id = response['uploadedFile']['id']; return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null); }).then((response) => { lastModified = response.headers['last-modified']; return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null); }).then((response) => { assert.strictEqual(response.headers['last-modified'], lastModified); }); }); it('should respond with Content-Range when requested after X bytes', () => { return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => { return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true}); }).then((response) => { const id = response['uploadedFile']['id']; return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null, {headers: {'Range': 'bytes=5-'}}); }).then((response) => { const headers = response.headers; assert.strictEqual(response.statusCode, 206); assert.strictEqual(headers['content-range'], 'bytes 5-11/12'); assert.strictEqual(response.responseText, 'content'); }); }); it('should respond with Content-Range when requested between X-Y bytes', () => { return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => { return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true}); }).then((response) => { const id = response['uploadedFile']['id']; return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null, {headers: {'Range': 'bytes=4-9'}}); }).then((response) => { const headers = response.headers; assert.strictEqual(response.statusCode, 206); assert.strictEqual(headers['content-range'], 'bytes 4-9/12'); assert.strictEqual(response.responseText, ' conte'); }); }); it('should respond with Content-Range when requested for the last X bytes', () => { return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => { return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true}); }).then((response) => { const id = response['uploadedFile']['id']; return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null, {headers: {'Range': 'bytes=-4'}}); }).then((response) => { const headers = response.headers; assert.strictEqual(response.statusCode, 206); assert.strictEqual(headers['content-range'], 'bytes 8-11/12'); assert.strictEqual(response.responseText, 'tent'); }); }); it('should respond with Content-Range for the whole content when the suffix length is larger than the content', () => { return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => { return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true}); }).then((response) => { const id = response['uploadedFile']['id']; return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null, {headers: {'Range': 'bytes=-100'}}); }).then((response) => { const headers = response.headers; assert.strictEqual(response.statusCode, 206); assert.strictEqual(headers['content-range'], 'bytes 0-11/12'); assert.strictEqual(response.responseText, 'some content'); }); }); it('should return 416 when the starting byte is after the file size', () => { return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => { return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true}); }).then((response) => { const id = response['uploadedFile']['id']; return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null, {headers: {'Range': 'bytes=12-'}}) .then(() => assert(false, 'should never be reached'), (error) => assert.strictEqual(error, 416)); }); }); it('should return 416 when the starting byte after the ending byte', () => { return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => { return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true}); }).then((response) => { const id = response['uploadedFile']['id']; return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null, {headers: {'Range': 'bytes=2-1'}}) .then(() => assert(false, 'should never be reached'), (error) => assert.strictEqual(error, 416)); }); }); it('should respond with Content-Range when If-Range matches the last modified date', () => { let id; return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => { return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true}); }).then((response) => { id = response['uploadedFile']['id']; return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null); }).then((response) => { assert.strictEqual(response.statusCode, 200); assert.strictEqual(response.responseText, 'some content'); return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null, {headers: {'Range': 'bytes = 9-10', 'If-Range': response.headers['last-modified']}}); }).then((response) => { const headers = response.headers; assert.strictEqual(response.statusCode, 206); assert.strictEqual(headers['content-range'], 'bytes 9-10/12'); assert.strictEqual(response.responseText, 'en'); }); }); it('should respond with Content-Range when If-Range matches ETag', () => { let id; return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => { return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true}); }).then((response) => { id = response['uploadedFile']['id']; return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null); }).then((response) => { assert.strictEqual(response.statusCode, 200); assert.strictEqual(response.responseText, 'some content'); return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null, {headers: {'Range': 'bytes = 9-10', 'If-Range': response.headers['etag']}}); }).then((response) => { const headers = response.headers; assert.strictEqual(response.statusCode, 206); assert.strictEqual(headers['content-range'], 'bytes 9-10/12'); assert.strictEqual(response.responseText, 'en'); }); }); it('should return the full content when If-Range does not match the last modified date or ETag', () => { let id; return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => { return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true}); }).then((response) => { id = response['uploadedFile']['id']; return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null); }).then((response) => { assert.strictEqual(response.statusCode, 200); assert.strictEqual(response.responseText, 'some content'); return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null, {'Range': 'bytes = 9-10', 'If-Range': 'foo'}); }).then((response) => { assert.strictEqual(response.statusCode, 200); assert.strictEqual(response.responseText, 'some content'); }); }); it('should respond with Content-Range across 64KB streaming chunks', () => { let id; const fileSize = 256 * 1024; const tokens = "0123456789abcdefghijklmnopqrstuvwxyz"; let buffer = Buffer.allocUnsafe(fileSize); for (let i = 0; i < fileSize; i++) buffer[i] = Math.floor(Math.random() * 256); let startByte = 63 * 1024; let endByte = 128 * 1024 - 1; let responseBufferList = []; const responseHandler = (response) => { response.on('data', (chunk) => responseBufferList.push(chunk)); }; function verifyBuffer() { const responseBuffer = Buffer.concat(responseBufferList); for (let i = 0; i < endByte - startByte + 1; i++) { const actual = responseBuffer[i]; const expected = buffer[startByte + i]; assert.strictEqual(actual, expected, `The byte at index ${i} should be identical. Expected ${expected} but got ${actual}`); } } return TemporaryFile.makeTemporaryFile('some.dat', buffer).then((stream) => { return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true}); }).then((response) => { id = response['uploadedFile']['id']; return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null, {headers: {'Range': `bytes = ${startByte}-${endByte}`}, responseHandler}); }).then((response) => { const headers = response.headers; assert.strictEqual(response.statusCode, 206); assert.strictEqual(headers['content-range'], `bytes ${startByte}-${endByte}/${fileSize}`); verifyBuffer(); }); }); });