// 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('works without Content-Type and extension', async () => {
      await reconfigureServer({
        fileUpload: {
          enableForPublic: true,
        },
      });
      const headers = {
        'X-Parse-Application-Id': 'test',
        'X-Parse-REST-API-Key': 'rest',
      };
      const result = await request({
        method: 'POST',
        headers: headers,
        url: 'http://localhost:8378/1/files/file',
        body: '<html></html>\n',
      });
      expect(result.data.url.includes('file.txt')).toBeTrue();
      expect(result.data.name.includes('file.txt')).toBeTrue();
    });

    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$/);
    });
  });
});
