UploadedFile should support a callback for upload progress
[WebKit.git] / Websites / perf.webkit.org / server-tests / api-uploaded-file.js
1 'use strict';
2
3 require('../tools/js/v3-models.js');
4
5 const assert = require('assert');
6 global.FormData = require('form-data');
7
8 const TestServer = require('./resources/test-server.js');
9 const TemporaryFile = require('./resources/temporary-file.js').TemporaryFile;
10
11 describe('/api/uploaded-file', function () {
12     this.timeout(5000);
13     TestServer.inject();
14
15     TemporaryFile.inject();
16
17     it('should return "InvalidArguments" when neither path nor sha256 query is set', () => {
18         return TestServer.remoteAPI().getJSON('/api/uploaded-file').then((content) => {
19             assert.equal(content['status'], 'InvalidArguments');
20             return TestServer.remoteAPI().getJSON('/api/uploaded-file/');
21         }).then((content) => {
22             assert.equal(content['status'], 'InvalidArguments');
23         });
24     });
25
26     it('should return 404 when there is no file with the specified ID', () => {
27         return TestServer.remoteAPI().getJSON('/api/uploaded-file/1').then((content) => {
28             assert(false, 'should never be reached');
29         }, (error) => {
30             assert.equal(error, 404);
31         });
32     });
33
34     it('should return 404 when the specified ID is not a valid integer', () => {
35         return TestServer.remoteAPI().getJSON('/api/uploaded-file/foo').then((content) => {
36             assert(false, 'should never be reached');
37         }, (error) => {
38             assert.equal(error, 404);
39         });
40     });
41
42     it('should return the file content matching the specified file ID', () => {
43         let uploadedFile;
44         return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => {
45             return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true});
46         }).then((response) => {
47             uploadedFile = response['uploadedFile'];
48             return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${uploadedFile['id']}`, 'GET', null, null);
49         }).then((response) => {
50             assert.equal(response.responseText, 'some content');
51         });
52     });
53
54     it('should return "NotFound" when the specified SHA256 is invalid', () => {
55         return TestServer.remoteAPI().getJSON('/api/uploaded-file/?sha256=abc').then((content) => {
56             assert.equal(content['status'], 'NotFound');
57         });
58     });
59
60     it('should return "NotFound" when there is no file matching the specified SHA256 ', () => {
61         return TestServer.remoteAPI().getJSON('/api/uploaded-file/?sha256=5256ec18f11624025905d057d6befb03d77b243511ac5f77ed5e0221ce6d84b5').then((content) => {
62             assert.equal(content['status'], 'NotFound');
63         });
64     });
65
66     it('should return the meta data of the file with the specified SHA256', () => {
67         let uploadedFile;
68         return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => {
69             return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true});
70         }).then((response) => {
71             uploadedFile = response['uploadedFile'];
72             return TestServer.remoteAPI().getJSON(`/api/uploaded-file/?sha256=${uploadedFile['sha256']}`);
73         }).then((response) => {
74             assert.deepEqual(uploadedFile, response['uploadedFile']);
75         });
76     });
77
78     it('should return "NotFound" when the file matching the specified SHA256 had already been deleted', () => {
79         let uploadedFile;
80         return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => {
81             return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true});
82         }).then((response) => {
83             uploadedFile = response['uploadedFile'];
84             const db = TestServer.database();
85             return db.connect().then(() => db.query(`UPDATE uploaded_files SET file_deleted_at = now() at time zone 'utc'`));
86         }).then(() => {
87             return TestServer.remoteAPI().getJSON(`/api/uploaded-file/?sha256=${uploadedFile['sha256']}`);
88         }).then((content) => {
89             assert.equal(content['status'], 'NotFound');
90         });
91     });
92
93
94     it('should respond with ETag, Acccept-Ranges, Content-Disposition, Content-Length, and Last-Modified headers', () => {
95         let uploadedFile;
96         return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => {
97             return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true});
98         }).then((response) => {
99             uploadedFile = response['uploadedFile'];
100             return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${uploadedFile['id']}`, 'GET', null, null);
101         }).then((response) => {
102             const headers = response.headers;
103
104             assert(Object.keys(headers).includes('etag'));
105             assert.equal(headers['etag'], uploadedFile['sha256']);
106
107             assert(Object.keys(headers).includes('accept-ranges'));
108             assert.equal(headers['accept-ranges'], 'bytes');
109
110             assert(Object.keys(headers).includes('content-disposition'));
111             assert.equal(headers['content-disposition'], `attachment; filename*=utf-8''some.dat`);
112
113             assert(Object.keys(headers).includes('content-length'));
114             assert.equal(headers['content-length'], uploadedFile['size']);
115
116             assert(Object.keys(headers).includes('last-modified'));
117         });
118     });
119
120     it('should respond with the same Last-Modified each time', () => {
121         let id;
122         let lastModified;
123         return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => {
124             return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true});
125         }).then((response) => {
126             id = response['uploadedFile']['id'];
127             return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null);
128         }).then((response) => {
129             lastModified = response.headers['last-modified'];
130             return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null);
131         }).then((response) => {
132             assert.equal(response.headers['last-modified'], lastModified);
133         });
134     });
135
136     it('should respond with Content-Range when requested after X bytes', () => {
137         return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => {
138             return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true});
139         }).then((response) => {
140             const id = response['uploadedFile']['id'];
141             return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null, {headers: {'Range': 'bytes=5-'}});
142         }).then((response) => {
143             const headers = response.headers;
144             assert.equal(response.statusCode, 206);
145             assert.equal(headers['content-range'], 'bytes 5-11/12');
146             assert.equal(response.responseText, 'content');
147         });
148     });
149
150     it('should respond with Content-Range when requested between X-Y bytes', () => {
151         return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => {
152             return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true});
153         }).then((response) => {
154             const id = response['uploadedFile']['id'];
155             return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null, {headers: {'Range': 'bytes=4-9'}});
156         }).then((response) => {
157             const headers = response.headers;
158             assert.equal(response.statusCode, 206);
159             assert.equal(headers['content-range'], 'bytes 4-9/12');
160             assert.equal(response.responseText, ' conte');
161         });
162     });
163
164     it('should respond with Content-Range when requested for the last X bytes', () => {
165         return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => {
166             return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true});
167         }).then((response) => {
168             const id = response['uploadedFile']['id'];
169             return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null, {headers: {'Range': 'bytes=-4'}});
170         }).then((response) => {
171             const headers = response.headers;
172             assert.equal(response.statusCode, 206);
173             assert.equal(headers['content-range'], 'bytes 8-11/12');
174             assert.equal(response.responseText, 'tent');
175         });
176     });
177
178     it('should respond with Content-Range for the whole content when the suffix length is larger than the content', () => {
179         return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => {
180             return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true});
181         }).then((response) => {
182             const id = response['uploadedFile']['id'];
183             return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null, {headers: {'Range': 'bytes=-100'}});
184         }).then((response) => {
185             const headers = response.headers;
186             assert.equal(response.statusCode, 206);
187             assert.equal(headers['content-range'], 'bytes 0-11/12');
188             assert.equal(response.responseText, 'some content');
189         });
190     });
191
192     it('should return 416 when the starting byte is after the file size', () => {
193         return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => {
194             return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true});
195         }).then((response) => {
196             const id = response['uploadedFile']['id'];
197             return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null, {headers: {'Range': 'bytes=12-'}})
198                 .then(() => assert(false, 'should never be reached'), (error) => assert.equal(error, 416));
199         });
200     });
201
202     it('should return 416 when the starting byte after the ending byte', () => {
203         return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => {
204             return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true});
205         }).then((response) => {
206             const id = response['uploadedFile']['id'];
207             return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null, {headers: {'Range': 'bytes=2-1'}})
208                 .then(() => assert(false, 'should never be reached'), (error) => assert.equal(error, 416));
209         });
210     });
211
212     it('should respond with Content-Range when If-Range matches the last modified date', () => {
213         let id;
214         return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => {
215             return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true});
216         }).then((response) => {
217             id = response['uploadedFile']['id'];
218             return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null);
219         }).then((response) => {
220             assert.equal(response.statusCode, 200);
221             assert.equal(response.responseText, 'some content');
222             return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null,
223                 {headers: {'Range': 'bytes = 9-10', 'If-Range': response.headers['last-modified']}});
224         }).then((response) => {
225             const headers = response.headers;
226             assert.equal(response.statusCode, 206);
227             assert.equal(headers['content-range'], 'bytes 9-10/12');
228             assert.equal(response.responseText, 'en');
229         });
230     });
231
232     it('should respond with Content-Range when If-Range matches ETag', () => {
233         let id;
234         return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => {
235             return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true});
236         }).then((response) => {
237             id = response['uploadedFile']['id'];
238             return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null);
239         }).then((response) => {
240             assert.equal(response.statusCode, 200);
241             assert.equal(response.responseText, 'some content');
242             return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null,
243                 {headers: {'Range': 'bytes = 9-10', 'If-Range': response.headers['etag']}});
244         }).then((response) => {
245             const headers = response.headers;
246             assert.equal(response.statusCode, 206);
247             assert.equal(headers['content-range'], 'bytes 9-10/12');
248             assert.equal(response.responseText, 'en');
249         });
250     });
251
252     it('should return the full content when If-Range does not match the last modified date or ETag', () => {
253         let id;
254         return TemporaryFile.makeTemporaryFile('some.dat', 'some content').then((stream) => {
255             return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true});
256         }).then((response) => {
257             id = response['uploadedFile']['id'];
258             return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null);
259         }).then((response) => {
260             assert.equal(response.statusCode, 200);
261             assert.equal(response.responseText, 'some content');
262             return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null,
263                 {'Range': 'bytes = 9-10', 'If-Range': 'foo'});
264         }).then((response) => {
265             assert.equal(response.statusCode, 200);
266             assert.equal(response.responseText, 'some content');
267         });
268     });
269
270     it('should respond with Content-Range across 64KB streaming chunks', () => {
271         let id;
272         const fileSize = 256 * 1024;
273         const tokens = "0123456789abcdefghijklmnopqrstuvwxyz";
274         let buffer = Buffer.allocUnsafe(fileSize);
275         for (let i = 0; i < fileSize; i++)
276             buffer[i] = Math.floor(Math.random() * 256);
277         let startByte = 63 * 1024;
278         let endByte = 128 * 1024 - 1;
279
280         let responseBufferList = [];
281         const responseHandler = (response) => {
282             response.on('data', (chunk) => responseBufferList.push(chunk));
283         };
284
285         function verifyBuffer()
286         {
287             const responseBuffer = Buffer.concat(responseBufferList);
288             for (let i = 0; i < endByte - startByte + 1; i++) {
289                 const actual = responseBuffer[i];
290                 const expected = buffer[startByte + i];
291                 assert.equal(actual, expected, `The byte at index ${i} should be identical. Expected ${expected} but got ${actual}`);
292             }
293         }
294
295         return TemporaryFile.makeTemporaryFile('some.dat', buffer).then((stream) => {
296             return PrivilegedAPI.sendRequest('upload-file', {newFile: stream}, {useFormData: true});
297         }).then((response) => {
298             id = response['uploadedFile']['id'];
299             return TestServer.remoteAPI().sendHttpRequest(`/api/uploaded-file/${id}`, 'GET', null, null,
300                 {headers: {'Range': `bytes = ${startByte}-${endByte}`}, responseHandler});
301         }).then((response) => {
302             const headers = response.headers;
303             assert.equal(response.statusCode, 206);
304             assert.equal(headers['content-range'], `bytes ${startByte}-${endByte}/${fileSize}`);
305             verifyBuffer();
306         });
307     });
308
309 });