Files
kami-parse-server/spec/ParseFile.spec.js
2024-07-16 10:34:10 +02:00

1524 lines
48 KiB
JavaScript

// This is a port of the test suite:
// hungry/js/test/parse_file_test.js
'use strict';
const { FilesController } = require('../lib/Controllers/FilesController');
const request = require('../lib/request');
const str = 'Hello World!';
const data = [];
for (let i = 0; i < str.length; i++) {
data.push(str.charCodeAt(i));
}
describe('Parse.File testing', () => {
describe('creating files', () => {
it('works with Content-Type', done => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/file.txt',
body: 'argle bargle',
}).then(response => {
const b = response.data;
expect(b.name).toMatch(/_file.txt$/);
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/);
request({ url: b.url }).then(response => {
const body = response.text;
expect(body).toEqual('argle bargle');
done();
});
});
});
it('works with _ContentType', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
fileExtensions: ['*'],
},
});
let response = await request({
method: 'POST',
url: 'http://localhost:8378/1/files/file',
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'text/html',
base64: 'PGh0bWw+PC9odG1sPgo=',
}),
});
const b = response.data;
expect(b.name).toMatch(/_file.html/);
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
response = await request({ url: b.url });
const body = response.text;
try {
expect(response.headers['content-type']).toMatch('^text/html');
expect(body).toEqual('<html></html>\n');
} catch (e) {
jfail(e);
}
});
it('works without Content-Type', done => {
const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/file.txt',
body: 'argle bargle',
}).then(response => {
const b = response.data;
expect(b.name).toMatch(/_file.txt$/);
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/);
request({ url: b.url }).then(response => {
expect(response.text).toEqual('argle bargle');
done();
});
});
});
it('supports REST end-to-end file create, read, delete, read', done => {
const headers = {
'Content-Type': 'image/jpeg',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/testfile.txt',
body: 'check one two',
}).then(response => {
const b = response.data;
expect(b.name).toMatch(/_testfile.txt$/);
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*testfile.txt$/);
request({ url: b.url }).then(response => {
const body = response.text;
expect(body).toEqual('check one two');
request({
method: 'DELETE',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Master-Key': 'test',
},
url: 'http://localhost:8378/1/files/' + b.name,
}).then(response => {
expect(response.status).toEqual(200);
request({
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
url: b.url,
}).then(fail, response => {
expect(response.status).toEqual(404);
done();
});
});
});
});
});
it('blocks file deletions with missing or incorrect master-key header', done => {
const headers = {
'Content-Type': 'image/jpeg',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/thefile.jpg',
body: 'the file body',
}).then(response => {
const b = response.data;
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*thefile.jpg$/);
// missing X-Parse-Master-Key header
request({
method: 'DELETE',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
url: 'http://localhost:8378/1/files/' + b.name,
}).then(fail, response => {
const del_b = response.data;
expect(response.status).toEqual(403);
expect(del_b.error).toMatch(/unauthorized/);
// incorrect X-Parse-Master-Key header
request({
method: 'DELETE',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Master-Key': 'tryagain',
},
url: 'http://localhost:8378/1/files/' + b.name,
}).then(fail, response => {
const del_b2 = response.data;
expect(response.status).toEqual(403);
expect(del_b2.error).toMatch(/unauthorized/);
done();
});
});
});
});
it('handles other filetypes', done => {
const headers = {
'Content-Type': 'image/jpeg',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/file.jpg',
body: 'argle bargle',
}).then(response => {
const b = response.data;
expect(b.name).toMatch(/_file.jpg$/);
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/.*file.jpg$/);
request({ url: b.url }).then(response => {
const body = response.text;
expect(body).toEqual('argle bargle');
done();
});
});
});
it('save file', async () => {
const file = new Parse.File('hello.txt', data, 'text/plain');
ok(!file.url());
const result = await file.save();
strictEqual(result, file);
ok(file.name());
ok(file.url());
notEqual(file.name(), 'hello.txt');
});
it('saves the file with tags', async () => {
spyOn(FilesController.prototype, 'createFile').and.callThrough();
const file = new Parse.File('hello.txt', data, 'text/plain');
const tags = { hello: 'world' };
file.setTags(tags);
expect(file.url()).toBeUndefined();
const result = await file.save();
expect(file.name()).toBeDefined();
expect(file.url()).toBeDefined();
expect(result.tags()).toEqual(tags);
expect(FilesController.prototype.createFile.calls.argsFor(0)[4]).toEqual({
tags: tags,
metadata: {},
});
});
it('does not pass empty file tags while saving', async () => {
spyOn(FilesController.prototype, 'createFile').and.callThrough();
const file = new Parse.File('hello.txt', data, 'text/plain');
expect(file.url()).toBeUndefined();
expect(file.name()).toBeDefined();
await file.save();
expect(file.url()).toBeDefined();
expect(FilesController.prototype.createFile.calls.argsFor(0)[4]).toEqual({
metadata: {},
});
});
it('save file in object', async done => {
const file = new Parse.File('hello.txt', data, 'text/plain');
ok(!file.url());
const result = await file.save();
strictEqual(result, file);
ok(file.name());
ok(file.url());
notEqual(file.name(), 'hello.txt');
const object = new Parse.Object('TestObject');
await object.save({ file: file });
const objectAgain = await new Parse.Query('TestObject').get(object.id);
ok(objectAgain.get('file') instanceof Parse.File);
done();
});
it('save file in object with escaped characters in filename', async () => {
const file = new Parse.File('hello . txt', data, 'text/plain');
ok(!file.url());
const result = await file.save();
strictEqual(result, file);
ok(file.name());
ok(file.url());
notEqual(file.name(), 'hello . txt');
const object = new Parse.Object('TestObject');
await object.save({ file });
const objectAgain = await new Parse.Query('TestObject').get(object.id);
ok(objectAgain.get('file') instanceof Parse.File);
});
it('autosave file in object', async done => {
let file = new Parse.File('hello.txt', data, 'text/plain');
ok(!file.url());
const object = new Parse.Object('TestObject');
await object.save({ file });
const objectAgain = await new Parse.Query('TestObject').get(object.id);
file = objectAgain.get('file');
ok(file instanceof Parse.File);
ok(file.name());
ok(file.url());
notEqual(file.name(), 'hello.txt');
done();
});
it('autosave file in object in object', async done => {
let file = new Parse.File('hello.txt', data, 'text/plain');
ok(!file.url());
const child = new Parse.Object('Child');
child.set('file', file);
const parent = new Parse.Object('Parent');
parent.set('child', child);
await parent.save();
const query = new Parse.Query('Parent');
query.include('child');
const parentAgain = await query.get(parent.id);
const childAgain = parentAgain.get('child');
file = childAgain.get('file');
ok(file instanceof Parse.File);
ok(file.name());
ok(file.url());
notEqual(file.name(), 'hello.txt');
done();
});
it('saving an already saved file', async () => {
const file = new Parse.File('hello.txt', data, 'text/plain');
ok(!file.url());
const result = await file.save();
strictEqual(result, file);
ok(file.name());
ok(file.url());
notEqual(file.name(), 'hello.txt');
const previousName = file.name();
await file.save();
equal(file.name(), previousName);
});
it('two saves at the same time', done => {
const file = new Parse.File('hello.txt', data, 'text/plain');
let firstName;
let secondName;
const firstSave = file.save().then(function () {
firstName = file.name();
});
const secondSave = file.save().then(function () {
secondName = file.name();
});
Promise.all([firstSave, secondSave]).then(
function () {
equal(firstName, secondName);
done();
},
function (error) {
ok(false, error);
done();
}
);
});
it('file toJSON testing', async () => {
const file = new Parse.File('hello.txt', data, 'text/plain');
ok(!file.url());
const object = new Parse.Object('TestObject');
await object.save({
file: file,
});
ok(object.toJSON().file.url);
});
it('content-type used with no extension', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
fileExtensions: ['*'],
},
});
const headers = {
'Content-Type': 'text/html',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
let response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/file',
body: 'fee fi fo',
});
const b = response.data;
expect(b.name).toMatch(/\.html$/);
response = await request({ url: b.url });
expect(response.headers['content-type']).toMatch(/^text\/html/);
});
it('filename is url encoded', done => {
const headers = {
'Content-Type': 'text/html',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/hello world.txt',
body: 'oh emm gee',
}).then(response => {
const b = response.data;
expect(b.url).toMatch(/hello%20world/);
done();
});
});
it('supports array of files', done => {
const file = {
__type: 'File',
url: 'http://meep.meep',
name: 'meep',
};
const files = [file, file];
const obj = new Parse.Object('FilesArrayTest');
obj.set('files', files);
obj
.save()
.then(() => {
const query = new Parse.Query('FilesArrayTest');
return query.first();
})
.then(result => {
const filesAgain = result.get('files');
expect(filesAgain.length).toEqual(2);
expect(filesAgain[0].name()).toEqual('meep');
expect(filesAgain[0].url()).toEqual('http://meep.meep');
done();
});
});
it('validates filename characters', done => {
const headers = {
'Content-Type': 'text/plain',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/di$avowed.txt',
body: 'will fail',
}).then(fail, response => {
const b = response.data;
expect(b.code).toEqual(122);
done();
});
});
it('validates filename length', done => {
const headers = {
'Content-Type': 'text/plain',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const fileName =
'Onceuponamidnightdrearywhileiponderedweak' +
'andwearyOveramanyquaintandcuriousvolumeof' +
'forgottenloreWhileinoddednearlynappingsud' +
'denlytherecameatapping';
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/' + fileName,
body: 'will fail',
}).then(fail, response => {
const b = response.data;
expect(b.code).toEqual(122);
done();
});
});
it('supports a dictionary with file', done => {
const file = {
__type: 'File',
url: 'http://meep.meep',
name: 'meep',
};
const dict = {
file: file,
};
const obj = new Parse.Object('FileObjTest');
obj.set('obj', dict);
obj
.save()
.then(() => {
const query = new Parse.Query('FileObjTest');
return query.first();
})
.then(result => {
const dictAgain = result.get('obj');
expect(typeof dictAgain).toEqual('object');
const fileAgain = dictAgain['file'];
expect(fileAgain.name()).toEqual('meep');
expect(fileAgain.url()).toEqual('http://meep.meep');
done();
})
.catch(e => {
jfail(e);
done();
});
});
it('creates correct url for old files hosted on files.parsetfss.com', done => {
const file = {
__type: 'File',
url: 'http://irrelevant.elephant/',
name: 'tfss-123.txt',
};
const obj = new Parse.Object('OldFileTest');
obj.set('oldfile', file);
obj
.save()
.then(() => {
const query = new Parse.Query('OldFileTest');
return query.first();
})
.then(result => {
const fileAgain = result.get('oldfile');
expect(fileAgain.url()).toEqual('http://files.parsetfss.com/test/tfss-123.txt');
done();
})
.catch(e => {
jfail(e);
done();
});
});
it('creates correct url for old files hosted on files.parse.com', done => {
const file = {
__type: 'File',
url: 'http://irrelevant.elephant/',
name: 'd6e80979-a128-4c57-a167-302f874700dc-123.txt',
};
const obj = new Parse.Object('OldFileTest');
obj.set('oldfile', file);
obj
.save()
.then(() => {
const query = new Parse.Query('OldFileTest');
return query.first();
})
.then(result => {
const fileAgain = result.get('oldfile');
expect(fileAgain.url()).toEqual(
'http://files.parse.com/test/d6e80979-a128-4c57-a167-302f874700dc-123.txt'
);
done();
})
.catch(e => {
jfail(e);
done();
});
});
it('supports files in objects without urls', done => {
const file = {
__type: 'File',
name: '123.txt',
};
const obj = new Parse.Object('FileTest');
obj.set('file', file);
obj
.save()
.then(() => {
const query = new Parse.Query('FileTest');
return query.first();
})
.then(result => {
const fileAgain = result.get('file');
expect(fileAgain.url()).toMatch(/123.txt$/);
done();
})
.catch(e => {
jfail(e);
done();
});
});
it('return with publicServerURL when provided', done => {
reconfigureServer({
publicServerURL: 'https://mydomain/parse',
})
.then(() => {
const file = {
__type: 'File',
name: '123.txt',
};
const obj = new Parse.Object('FileTest');
obj.set('file', file);
return obj.save();
})
.then(() => {
const query = new Parse.Query('FileTest');
return query.first();
})
.then(result => {
const fileAgain = result.get('file');
expect(fileAgain.url().indexOf('https://mydomain/parse')).toBe(0);
done();
})
.catch(e => {
jfail(e);
done();
});
});
it('fails to upload an empty file', done => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/file.txt',
body: '',
}).then(fail, response => {
expect(response.status).toBe(400);
const body = response.text;
expect(body).toEqual('{"code":130,"error":"Invalid file upload."}');
done();
});
});
it('fails to upload without a file name', done => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/',
body: 'yolo',
}).then(fail, response => {
expect(response.status).toBe(400);
const body = response.text;
expect(body).toEqual('{"code":122,"error":"Filename not provided."}');
done();
});
});
});
describe('deleting files', () => {
it('fails to delete an unkown file', done => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Master-Key': 'test',
};
request({
method: 'DELETE',
headers: headers,
url: 'http://localhost:8378/1/files/file.txt',
}).then(fail, response => {
expect(response.status).toBe(400);
const body = response.text;
expect(typeof body).toBe('string');
const { code, error } = JSON.parse(body);
expect(code).toBe(153);
expect(typeof error).toBe('string');
expect(error.length).toBeGreaterThan(0);
done();
});
});
});
describe('getting files', () => {
it('does not crash on file request with invalid app ID', async () => {
const res1 = await request({
url: 'http://localhost:8378/1/files/invalid-id/invalid-file.txt',
}).catch(e => e);
expect(res1.status).toBe(403);
expect(res1.data).toEqual({ code: 119, error: 'Invalid application ID.' });
// Ensure server did not crash
const res2 = await request({ url: 'http://localhost:8378/1/health' });
expect(res2.status).toEqual(200);
expect(res2.data).toEqual({ status: 'ok' });
});
it('does not crash on file request with invalid path', async () => {
const res1 = await request({
url: 'http://localhost:8378/1/files/invalid-id//invalid-path/%20/invalid-file.txt',
}).catch(e => e);
expect(res1.status).toBe(403);
expect(res1.data).toEqual({ error: 'unauthorized' });
// Ensure server did not crash
const res2 = await request({ url: 'http://localhost:8378/1/health' });
expect(res2.status).toEqual(200);
expect(res2.data).toEqual({ status: 'ok' });
});
it('does not crash on file metadata request with invalid app ID', async () => {
const res1 = await request({
url: `http://localhost:8378/1/files/invalid-id/metadata/invalid-file.txt`,
});
expect(res1.status).toBe(200);
expect(res1.data).toEqual({});
// Ensure server did not crash
const res2 = await request({ url: 'http://localhost:8378/1/health' });
expect(res2.status).toEqual(200);
expect(res2.data).toEqual({ status: 'ok' });
});
});
describe_only_db('mongo')('Gridstore Range', () => {
it('supports bytes range out of range', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: repeat('argle bargle', 100),
});
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
Range: 'bytes=15000-18000',
},
});
expect(file.headers['content-range']).toBe('bytes 1212-1212/1212');
});
it('supports bytes range if end greater than start', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: repeat('argle bargle', 100),
});
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
Range: 'bytes=15000-100',
},
});
expect(file.headers['content-range']).toBe('bytes 100-1212/1212');
});
it('supports bytes range if end is undefined', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: repeat('argle bargle', 100),
});
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
Range: 'bytes=100-',
},
});
expect(file.headers['content-range']).toBe('bytes 100-1212/1212');
});
it('supports bytes range if start and end undefined', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: repeat('argle bargle', 100),
});
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
},
}).catch(e => e);
expect(file.headers['content-range']).toBeUndefined();
});
it('supports bytes range if end is greater than size', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: repeat('argle bargle', 100),
});
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
Range: 'bytes=0-2000',
},
}).catch(e => e);
expect(file.headers['content-range']).toBe('bytes 0-1212/1212');
});
it('supports bytes range with 0 length', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: 'a',
}).catch(e => e);
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
Range: 'bytes=-2000',
},
}).catch(e => e);
expect(file.headers['content-range']).toBe('bytes 0-1/1');
});
it('supports range requests', done => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/file.txt',
body: 'argle bargle',
}).then(response => {
const b = response.data;
request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
Range: 'bytes=0-5',
},
}).then(response => {
const body = response.text;
expect(body).toEqual('argle ');
done();
});
});
});
it('supports small range requests', done => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/file.txt',
body: 'argle bargle',
}).then(response => {
const b = response.data;
request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
Range: 'bytes=0-2',
},
}).then(response => {
const body = response.text;
expect(body).toEqual('arg');
done();
});
});
});
// See specs https://www.greenbytes.de/tech/webdav/draft-ietf-httpbis-p5-range-latest.html#byte.ranges
it('supports getting one byte', done => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/file.txt',
body: 'argle bargle',
}).then(response => {
const b = response.data;
request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
Range: 'bytes=2-2',
},
}).then(response => {
const body = response.text;
expect(body).toEqual('g');
done();
});
});
});
it('supports getting last n bytes', done => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/file.txt',
body: 'something different',
}).then(response => {
const b = response.data;
request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
Range: 'bytes=-4',
},
}).then(response => {
const body = response.text;
expect(body.length).toBe(4);
expect(body).toEqual('rent');
done();
});
});
});
it('supports getting first n bytes', done => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/file.txt',
body: 'something different',
}).then(response => {
const b = response.data;
request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
Range: 'bytes=10-',
},
}).then(response => {
const body = response.text;
expect(body).toEqual('different');
done();
});
});
});
function repeat(string, count) {
let s = string;
while (count > 0) {
s += string;
count--;
}
return s;
}
it('supports large range requests', done => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/file.txt',
body: repeat('argle bargle', 100),
}).then(response => {
const b = response.data;
request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
Range: 'bytes=13-240',
},
}).then(response => {
const body = response.text;
expect(body.length).toEqual(228);
expect(body.indexOf('rgle barglea')).toBe(0);
done();
});
});
});
it('fails to stream unknown file', async () => {
const response = await request({
url: 'http://localhost:8378/1/files/test/file.txt',
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
Range: 'bytes=13-240',
},
}).catch(e => e);
expect(response.status).toBe(404);
const body = response.text;
expect(body).toEqual('File not found.');
});
});
// Because GridStore is not loaded on PG, those are perfect
// for fallback tests
describe_only_db('postgres')('Default Range tests', () => {
it('fallback to regular request', async done => {
await reconfigureServer();
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/file.txt',
body: 'argle bargle',
}).then(response => {
const b = response.data;
request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
Range: 'bytes=0-5',
},
}).then(response => {
const body = response.text;
expect(body).toEqual('argle bargle');
done();
});
});
});
});
describe('file upload configuration', () => {
it('allows file upload only for authenticated user by default', async () => {
await reconfigureServer({
fileUpload: {},
});
let file = new Parse.File('hello.txt', data, 'text/plain');
await expectAsync(file.save()).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by public is disabled.')
);
file = new Parse.File('hello.txt', data, 'text/plain');
const anonUser = await Parse.AnonymousUtils.logIn();
await expectAsync(file.save({ sessionToken: anonUser.getSessionToken() })).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by anonymous user is disabled.')
);
file = new Parse.File('hello.txt', data, 'text/plain');
const authUser = await Parse.User.signUp('user', 'password');
await expectAsync(file.save({ sessionToken: authUser.getSessionToken() })).toBeResolved();
});
it('allows file upload with master key', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: false,
enableForAnonymousUser: false,
enableForAuthenticatedUser: false,
},
});
const file = new Parse.File('hello.txt', data, 'text/plain');
await expectAsync(file.save({ useMasterKey: true })).toBeResolved();
});
it('rejects all file uploads', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: false,
enableForAnonymousUser: false,
enableForAuthenticatedUser: false,
},
});
let file = new Parse.File('hello.txt', data, 'text/plain');
await expectAsync(file.save()).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by public is disabled.')
);
file = new Parse.File('hello.txt', data, 'text/plain');
const anonUser = await Parse.AnonymousUtils.logIn();
await expectAsync(file.save({ sessionToken: anonUser.getSessionToken() })).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by anonymous user is disabled.')
);
file = new Parse.File('hello.txt', data, 'text/plain');
const authUser = await Parse.User.signUp('user', 'password');
await expectAsync(file.save({ sessionToken: authUser.getSessionToken() })).toBeRejectedWith(
new Parse.Error(
Parse.Error.FILE_SAVE_ERROR,
'File upload by authenticated user is disabled.'
)
);
});
it('allows all file uploads', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
enableForAnonymousUser: true,
enableForAuthenticatedUser: true,
},
});
let file = new Parse.File('hello.txt', data, 'text/plain');
await expectAsync(file.save()).toBeResolved();
file = new Parse.File('hello.txt', data, 'text/plain');
const anonUser = await Parse.AnonymousUtils.logIn();
await expectAsync(file.save({ sessionToken: anonUser.getSessionToken() })).toBeResolved();
file = new Parse.File('hello.txt', data, 'text/plain');
const authUser = await Parse.User.signUp('user', 'password');
await expectAsync(file.save({ sessionToken: authUser.getSessionToken() })).toBeResolved();
});
it('allows file upload only for public', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
enableForAnonymousUser: false,
enableForAuthenticatedUser: false,
},
});
let file = new Parse.File('hello.txt', data, 'text/plain');
await expectAsync(file.save()).toBeResolved();
file = new Parse.File('hello.txt', data, 'text/plain');
const anonUser = await Parse.AnonymousUtils.logIn();
await expectAsync(file.save({ sessionToken: anonUser.getSessionToken() })).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by anonymous user is disabled.')
);
file = new Parse.File('hello.txt', data, 'text/plain');
const authUser = await Parse.User.signUp('user', 'password');
await expectAsync(file.save({ sessionToken: authUser.getSessionToken() })).toBeRejectedWith(
new Parse.Error(
Parse.Error.FILE_SAVE_ERROR,
'File upload by authenticated user is disabled.'
)
);
});
it('allows file upload only for anonymous user', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: false,
enableForAnonymousUser: true,
enableForAuthenticatedUser: false,
},
});
let file = new Parse.File('hello.txt', data, 'text/plain');
await expectAsync(file.save()).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by public is disabled.')
);
file = new Parse.File('hello.txt', data, 'text/plain');
const anonUser = await Parse.AnonymousUtils.logIn();
await expectAsync(file.save({ sessionToken: anonUser.getSessionToken() })).toBeResolved();
file = new Parse.File('hello.txt', data, 'text/plain');
const authUser = await Parse.User.signUp('user', 'password');
await expectAsync(file.save({ sessionToken: authUser.getSessionToken() })).toBeRejectedWith(
new Parse.Error(
Parse.Error.FILE_SAVE_ERROR,
'File upload by authenticated user is disabled.'
)
);
});
it('allows file upload only for authenticated user', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: false,
enableForAnonymousUser: false,
enableForAuthenticatedUser: true,
},
});
let file = new Parse.File('hello.txt', data, 'text/plain');
await expectAsync(file.save()).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by public is disabled.')
);
file = new Parse.File('hello.txt', data, 'text/plain');
const anonUser = await Parse.AnonymousUtils.logIn();
await expectAsync(file.save({ sessionToken: anonUser.getSessionToken() })).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by anonymous user is disabled.')
);
file = new Parse.File('hello.txt', data, 'text/plain');
const authUser = await Parse.User.signUp('user', 'password');
await expectAsync(file.save({ sessionToken: authUser.getSessionToken() })).toBeResolved();
});
it('rejects invalid fileUpload configuration', async () => {
const invalidConfigs = [
{ fileUpload: undefined },
{ fileUpload: null },
{ fileUpload: [] },
{ fileUpload: 1 },
{ fileUpload: 'string' },
];
const validConfigs = [{ fileUpload: {} }];
const keys = ['enableForPublic', 'enableForAnonymousUser', 'enableForAuthenticatedUser'];
const invalidValues = [[], {}, 1, 'string', null];
const validValues = [undefined, true, false];
for (const config of invalidConfigs) {
await expectAsync(reconfigureServer(config)).toBeRejectedWith(
'fileUpload must be an object value.'
);
}
for (const config of validConfigs) {
await expectAsync(reconfigureServer(config)).toBeResolved();
}
for (const key of keys) {
for (const value of invalidValues) {
await expectAsync(reconfigureServer({ fileUpload: { [key]: value } })).toBeRejectedWith(
`fileUpload.${key} must be a boolean value.`
);
}
for (const value of validValues) {
await expectAsync(reconfigureServer({ fileUpload: { [key]: value } })).toBeResolved();
}
}
await expectAsync(
reconfigureServer({
fileUpload: {
fileExtensions: 1,
},
})
).toBeRejectedWith('fileUpload.fileExtensions must be an array.');
});
});
describe('fileExtensions', () => {
it('works with _ContentType', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
fileExtensions: ['png'],
},
});
await expectAsync(
request({
method: 'POST',
url: 'http://localhost:8378/1/files/file',
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'text/html',
base64: 'PGh0bWw+PC9odG1sPgo=',
}),
}).catch(e => {
throw new Error(e.data.error);
})
).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
);
});
it('works without Content-Type', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
},
});
const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
await expectAsync(
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/file.html',
body: '<html></html>\n',
}).catch(e => {
throw new Error(e.data.error);
})
).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
);
});
it('default should allow common types', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
},
});
for (const type of ['plain', 'txt', 'png', 'jpg', 'gif', 'doc']) {
const file = new Parse.File(`parse-server-logo.${type}`, { base64: 'ParseA==' });
await file.save();
}
});
it('works with a period in the file name', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
fileExtensions: ['^[^hH][^tT][^mM][^lL]?$'],
},
});
const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const values = ['file.png.html', 'file.txt.png.html', 'file.png.txt.html'];
for (const value of values) {
await expectAsync(
request({
method: 'POST',
headers: headers,
url: `http://localhost:8378/1/files/${value}`,
body: '<html></html>\n',
}).catch(e => {
throw new Error(e.data.error);
})
).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
);
}
});
it('works to stop invalid filenames', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
fileExtensions: ['^[^hH][^tT][^mM][^lL]?$'],
},
});
const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const values = [
'!invalid.png',
'.png',
'.html',
' .html',
'.png.html',
'~invalid.png',
'-invalid.png',
];
for (const value of values) {
await expectAsync(
request({
method: 'POST',
headers: headers,
url: `http://localhost:8378/1/files/${value}`,
body: '<html></html>\n',
}).catch(e => {
throw new Error(e.data.error);
})
).toBeRejectedWith(
new Parse.Error(Parse.Error.INVALID_FILE_NAME, `Filename contains invalid characters.`)
);
}
});
it('allows file without extension', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
fileExtensions: ['^[^hH][^tT][^mM][^lL]?$'],
},
});
const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const values = ['filenamewithoutextension'];
for (const value of values) {
await expectAsync(
request({
method: 'POST',
headers: headers,
url: `http://localhost:8378/1/files/${value}`,
body: '<html></html>\n',
}).catch(e => {
throw new Error(e.data.error);
})
).toBeResolved();
}
});
it('works with array', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
fileExtensions: ['jpg', 'wav'],
},
});
await expectAsync(
request({
method: 'POST',
url: 'http://localhost:8378/1/files/file',
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'text/html',
base64: 'PGh0bWw+PC9odG1sPgo=',
}),
}).catch(e => {
throw new Error(e.data.error);
})
).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
);
await expectAsync(
request({
method: 'POST',
url: 'http://localhost:8378/1/files/file',
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'image/jpg',
base64: 'PGh0bWw+PC9odG1sPgo=',
}),
})
).toBeResolved();
await expectAsync(
request({
method: 'POST',
url: 'http://localhost:8378/1/files/file',
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'audio/wav',
base64: 'UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA',
}),
})
).toBeResolved();
});
it('works with array without Content-Type', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
fileExtensions: ['jpg'],
},
});
const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
await expectAsync(
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/file.html',
body: '<html></html>\n',
}).catch(e => {
throw new Error(e.data.error);
})
).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
);
});
it('works with array with correct file type', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
fileExtensions: ['html'],
},
});
const response = await request({
method: 'POST',
url: 'http://localhost:8378/1/files/file',
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'text/html',
base64: 'PGh0bWw+PC9odG1sPgo=',
}),
});
const b = response.data;
expect(b.name).toMatch(/_file.html$/);
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
});
});
});