'use strict'; // These tests check the "create" / "update" functionality of the REST API. const auth = require('../lib/Auth'); const Config = require('../lib/Config'); const Parse = require('parse/node').Parse; const rest = require('../lib/rest'); const RestWrite = require('../lib/RestWrite'); const request = require('../lib/request'); let config; let database; describe('rest create', () => { let loggerErrorSpy; beforeEach(() => { config = Config.get('test'); database = config.database; const logger = require('../lib/logger').default; loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); }); it('handles _id', done => { rest .create(config, auth.nobody(config), 'Foo', {}) .then(() => database.adapter.find('Foo', { fields: {} }, {}, {})) .then(results => { expect(results.length).toEqual(1); const obj = results[0]; expect(typeof obj.objectId).toEqual('string'); expect(obj.objectId.length).toEqual(10); expect(obj._id).toBeUndefined(); done(); }); }); it('can use custom _id size', done => { config.objectIdSize = 20; rest .create(config, auth.nobody(config), 'Foo', {}) .then(() => database.adapter.find('Foo', { fields: {} }, {}, {})) .then(results => { expect(results.length).toEqual(1); const obj = results[0]; expect(typeof obj.objectId).toEqual('string'); expect(obj.objectId.length).toEqual(20); done(); }); }); it('should use objectId from client when allowCustomObjectId true', async () => { config.allowCustomObjectId = true; // use time as unique custom id for test reusability const customId = `${Date.now()}`; const obj = { objectId: customId, }; const { status, response: { objectId }, } = await rest.create(config, auth.nobody(config), 'MyClass', obj); expect(status).toEqual(201); expect(objectId).toEqual(customId); }); it('should throw on invalid objectId when allowCustomObjectId true', () => { config.allowCustomObjectId = true; const objIdNull = { objectId: null, }; const objIdUndef = { objectId: undefined, }; const objIdEmpty = { objectId: '', }; const err = 'objectId must not be empty, null or undefined'; expect(() => rest.create(config, auth.nobody(config), 'MyClass', objIdEmpty)).toThrowError(err); expect(() => rest.create(config, auth.nobody(config), 'MyClass', objIdNull)).toThrowError(err); expect(() => rest.create(config, auth.nobody(config), 'MyClass', objIdUndef)).toThrowError(err); }); it('should generate objectId when not set by client with allowCustomObjectId true', async () => { config.allowCustomObjectId = true; const { status, response: { objectId }, } = await rest.create(config, auth.nobody(config), 'MyClass', {}); expect(status).toEqual(201); expect(objectId).toBeDefined(); }); it('is backwards compatible when _id size changes', done => { rest .create(config, auth.nobody(config), 'Foo', { size: 10 }) .then(() => { config.objectIdSize = 20; return rest.find(config, auth.nobody(config), 'Foo', { size: 10 }); }) .then(response => { expect(response.results.length).toEqual(1); expect(response.results[0].objectId.length).toEqual(10); return rest.update( config, auth.nobody(config), 'Foo', { objectId: response.results[0].objectId }, { update: 20 } ); }) .then(() => { return rest.find(config, auth.nobody(config), 'Foo', { size: 10 }); }) .then(response => { expect(response.results.length).toEqual(1); expect(response.results[0].objectId.length).toEqual(10); expect(response.results[0].update).toEqual(20); return rest.create(config, auth.nobody(config), 'Foo', { size: 20 }); }) .then(() => { config.objectIdSize = 10; return rest.find(config, auth.nobody(config), 'Foo', { size: 20 }); }) .then(response => { expect(response.results.length).toEqual(1); expect(response.results[0].objectId.length).toEqual(20); done(); }); }); describe('with maintenance key', () => { let req; async function getObject(id) { const res = await request({ headers: { 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', }, method: 'GET', url: `http://localhost:8378/1/classes/TestObject/${id}`, }); return res.data; } beforeEach(() => { req = { headers: { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', 'X-Parse-Maintenance-Key': 'testing', }, method: 'POST', url: 'http://localhost:8378/1/classes/TestObject', }; }); it('allows createdAt', async () => { const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' }; req.body = { createdAt }; const res = await request(req); expect(res.data.createdAt).toEqual(createdAt.iso); }); it('allows createdAt and updatedAt', async () => { const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' }; const updatedAt = { __type: 'Date', iso: '2019-02-01T00:00:00.000Z' }; req.body = { createdAt, updatedAt }; const res = await request(req); const obj = await getObject(res.data.objectId); expect(obj.createdAt).toEqual(createdAt.iso); expect(obj.updatedAt).toEqual(updatedAt.iso); }); it('allows createdAt, updatedAt, and additional field', async () => { const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' }; const updatedAt = { __type: 'Date', iso: '2019-02-01T00:00:00.000Z' }; req.body = { createdAt, updatedAt, testing: 123 }; const res = await request(req); const obj = await getObject(res.data.objectId); expect(obj.createdAt).toEqual(createdAt.iso); expect(obj.updatedAt).toEqual(updatedAt.iso); expect(obj.testing).toEqual(123); }); it('cannot set updatedAt dated before createdAt', async () => { const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' }; const updatedAt = { __type: 'Date', iso: '2018-12-01T00:00:00.000Z' }; req.body = { createdAt, updatedAt }; try { await request(req); fail(); } catch (err) { expect(err.data.code).toEqual(Parse.Error.VALIDATION_ERROR); } }); it('cannot set updatedAt without createdAt', async () => { const updatedAt = { __type: 'Date', iso: '2018-12-01T00:00:00.000Z' }; req.body = { updatedAt }; const res = await request(req); const obj = await getObject(res.data.objectId); expect(obj.updatedAt).not.toEqual(updatedAt.iso); }); it('handles bad types for createdAt and updatedAt', async () => { const createdAt = 12345; const updatedAt = true; req.body = { createdAt, updatedAt }; try { await request(req); fail(); } catch (err) { expect(err.data.code).toEqual(Parse.Error.INCORRECT_TYPE); } }); it('cannot set createdAt or updatedAt without maintenance key', async () => { const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' }; const updatedAt = { __type: 'Date', iso: '2019-02-01T00:00:00.000Z' }; req.body = { createdAt, updatedAt }; delete req.headers['X-Parse-Maintenance-Key']; const res = await request(req); expect(res.data.createdAt).not.toEqual(createdAt.iso); expect(res.data.updatedAt).not.toEqual(updatedAt.iso); }); }); it_id('6c30306f-328c-47f2-88a7-2deffaee997f')(it)('handles array, object, date', done => { const now = new Date(); const obj = { array: [1, 2, 3], object: { foo: 'bar' }, date: Parse._encode(now), }; rest .create(config, auth.nobody(config), 'MyClass', obj) .then(() => database.adapter.find( 'MyClass', { fields: { array: { type: 'Array' }, object: { type: 'Object' }, date: { type: 'Date' }, }, }, {}, {} ) ) .then(results => { expect(results.length).toEqual(1); const mob = results[0]; expect(mob.array instanceof Array).toBe(true); expect(typeof mob.object).toBe('object'); expect(mob.date.__type).toBe('Date'); expect(new Date(mob.date.iso).getTime()).toBe(now.getTime()); done(); }); }); it('handles object and subdocument', done => { const obj = { subdoc: { foo: 'bar', wu: 'tan' } }; Parse.Cloud.beforeSave('MyClass', function () { // this beforeSave trigger should do nothing but can mess with the object }); rest .create(config, auth.nobody(config), 'MyClass', obj) .then(() => database.adapter.find('MyClass', { fields: {} }, {}, {})) .then(results => { expect(results.length).toEqual(1); const mob = results[0]; expect(typeof mob.subdoc).toBe('object'); expect(mob.subdoc.foo).toBe('bar'); expect(mob.subdoc.wu).toBe('tan'); expect(typeof mob.objectId).toEqual('string'); const obj = { 'subdoc.wu': 'clan' }; return rest.update(config, auth.nobody(config), 'MyClass', { objectId: mob.objectId }, obj); }) .then(() => database.adapter.find('MyClass', { fields: {} }, {}, {})) .then(results => { expect(results.length).toEqual(1); const mob = results[0]; expect(typeof mob.subdoc).toBe('object'); expect(mob.subdoc.foo).toBe('bar'); expect(mob.subdoc.wu).toBe('clan'); done(); }) .catch(done.fail); }); it('handles create on non-existent class when disabled client class creation', done => { const customConfig = Object.assign({}, config, { allowClientClassCreation: false, }); loggerErrorSpy.calls.reset(); rest.create(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {}).then( () => { fail('Should throw an error'); done(); }, err => { expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); expect(err.message).toEqual('Permission denied'); expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('This user is not allowed to access ' + 'non-existent class: ClientClassCreation')); done(); } ); }); it('handles create on existent class when disabled client class creation', async () => { const customConfig = Object.assign({}, config, { allowClientClassCreation: false, }); const schema = await config.database.loadSchema(); const actualSchema = await schema.addClassIfNotExists('ClientClassCreation', {}); expect(actualSchema.className).toEqual('ClientClassCreation'); await schema.reloadData({ clearCache: true }); // Should not throw await rest.create(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {}); }); it('handles user signup', done => { const user = { username: 'asdf', password: 'zxcv', foo: 'bar', }; rest.create(config, auth.nobody(config), '_User', user).then(r => { expect(Object.keys(r.response).length).toEqual(3); expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.sessionToken).toEqual('string'); done(); }); }); it('handles anonymous user signup', done => { const data1 = { authData: { anonymous: { id: '00000000-0000-0000-0000-000000000001', }, }, }; const data2 = { authData: { anonymous: { id: '00000000-0000-0000-0000-000000000002', }, }, }; let username1; rest .create(config, auth.nobody(config), '_User', data1) .then(r => { expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.sessionToken).toEqual('string'); expect(typeof r.response.username).toEqual('string'); return rest.create(config, auth.nobody(config), '_User', data1); }) .then(r => { expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.username).toEqual('string'); expect(typeof r.response.updatedAt).toEqual('string'); username1 = r.response.username; return rest.create(config, auth.nobody(config), '_User', data2); }) .then(r => { expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.sessionToken).toEqual('string'); return rest.create(config, auth.nobody(config), '_User', data2); }) .then(r => { expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.username).toEqual('string'); expect(typeof r.response.updatedAt).toEqual('string'); expect(r.response.username).not.toEqual(username1); done(); }); }); it('handles anonymous user signup and upgrade to new user', done => { const data1 = { authData: { anonymous: { id: '00000000-0000-0000-0000-000000000001', }, }, }; const updatedData = { authData: { anonymous: null }, username: 'hello', password: 'world', }; let objectId; rest .create(config, auth.nobody(config), '_User', data1) .then(r => { expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.sessionToken).toEqual('string'); objectId = r.response.objectId; return auth.getAuthForSessionToken({ config, sessionToken: r.response.sessionToken, }); }) .then(sessionAuth => { return rest.update(config, sessionAuth, '_User', { objectId }, updatedData); }) .then(() => { return Parse.User.logOut().then(() => { return Parse.User.logIn('hello', 'world'); }); }) .then(r => { expect(r.id).toEqual(objectId); expect(r.get('username')).toEqual('hello'); done(); }) .catch(err => { jfail(err); done(); }); }); it('handles no anonymous users config', done => { const NoAnnonConfig = Object.assign({}, config); NoAnnonConfig.authDataManager.setEnableAnonymousUsers(false); const data1 = { authData: { anonymous: { id: '00000000-0000-0000-0000-000000000001', }, }, }; rest.create(NoAnnonConfig, auth.nobody(NoAnnonConfig), '_User', data1).then( () => { fail('Should throw an error'); done(); }, err => { expect(err.code).toEqual(Parse.Error.UNSUPPORTED_SERVICE); expect(err.message).toEqual('This authentication method is unsupported.'); NoAnnonConfig.authDataManager.setEnableAnonymousUsers(true); done(); } ); }); it('test facebook signup and login', done => { const data = { authData: { facebook: { id: '8675309', access_token: 'jenny', }, }, }; let newUserSignedUpByFacebookObjectId; rest .create(config, auth.nobody(config), '_User', data) .then(r => { expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.sessionToken).toEqual('string'); newUserSignedUpByFacebookObjectId = r.response.objectId; return rest.create(config, auth.nobody(config), '_User', data); }) .then(r => { expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.username).toEqual('string'); expect(typeof r.response.updatedAt).toEqual('string'); expect(r.response.objectId).toEqual(newUserSignedUpByFacebookObjectId); return rest.find(config, auth.master(config), '_Session', { sessionToken: r.response.sessionToken, }); }) .then(response => { expect(response.results.length).toEqual(1); const output = response.results[0]; expect(output.user.objectId).toEqual(newUserSignedUpByFacebookObjectId); done(); }) .catch(err => { jfail(err); done(); }); }); it('stores pointers', done => { const obj = { foo: 'bar', aPointer: { __type: 'Pointer', className: 'JustThePointer', objectId: 'qwerty1234', // make it 10 chars to match PG storage }, }; rest .create(config, auth.nobody(config), 'APointerDarkly', obj) .then(() => database.adapter.find( 'APointerDarkly', { fields: { foo: { type: 'String' }, aPointer: { type: 'Pointer', targetClass: 'JustThePointer' }, }, }, {}, {} ) ) .then(results => { expect(results.length).toEqual(1); const output = results[0]; expect(typeof output.foo).toEqual('string'); expect(typeof output._p_aPointer).toEqual('undefined'); expect(output._p_aPointer).toBeUndefined(); expect(output.aPointer).toEqual({ __type: 'Pointer', className: 'JustThePointer', objectId: 'qwerty1234', }); done(); }); }); it('stores pointers to objectIds larger than 10 characters', done => { const obj = { foo: 'bar', aPointer: { __type: 'Pointer', className: 'JustThePointer', objectId: '49F62F92-9B56-46E7-A3D4-BBD14C52F666', }, }; rest .create(config, auth.nobody(config), 'APointerDarkly', obj) .then(() => database.adapter.find( 'APointerDarkly', { fields: { foo: { type: 'String' }, aPointer: { type: 'Pointer', targetClass: 'JustThePointer' }, }, }, {}, {} ) ) .then(results => { expect(results.length).toEqual(1); const output = results[0]; expect(typeof output.foo).toEqual('string'); expect(typeof output._p_aPointer).toEqual('undefined'); expect(output._p_aPointer).toBeUndefined(); expect(output.aPointer).toEqual({ __type: 'Pointer', className: 'JustThePointer', objectId: '49F62F92-9B56-46E7-A3D4-BBD14C52F666', }); done(); }); }); it('cannot set objectId', done => { const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', }; request({ headers: headers, method: 'POST', url: 'http://localhost:8378/1/classes/TestObject', body: JSON.stringify({ foo: 'bar', objectId: 'hello', }), }).then(fail, response => { const b = response.data; expect(b.code).toEqual(105); expect(b.error).toEqual('objectId is an invalid field name.'); done(); }); }); it('cannot set id', done => { const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', }; request({ headers: headers, method: 'POST', url: 'http://localhost:8378/1/classes/TestObject', body: JSON.stringify({ foo: 'bar', id: 'hello', }), }).then(fail, response => { const b = response.data; expect(b.code).toEqual(105); expect(b.error).toEqual('id is an invalid field name.'); done(); }); }); it('test default session length', done => { const user = { username: 'asdf', password: 'zxcv', foo: 'bar', }; const now = new Date(); rest .create(config, auth.nobody(config), '_User', user) .then(r => { expect(Object.keys(r.response).length).toEqual(3); expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.sessionToken).toEqual('string'); return rest.find(config, auth.master(config), '_Session', { sessionToken: r.response.sessionToken, }); }) .then(r => { expect(r.results.length).toEqual(1); const session = r.results[0]; const actual = new Date(session.expiresAt.iso); const expected = new Date(now.getTime() + 1000 * 3600 * 24 * 365); expect(Math.abs(actual - expected) <= jasmine.DEFAULT_TIMEOUT_INTERVAL).toEqual(true); done(); }); }); it('test specified session length', done => { const user = { username: 'asdf', password: 'zxcv', foo: 'bar', }; const sessionLength = 3600, // 1 Hour ahead now = new Date(); // For reference later config.sessionLength = sessionLength; rest .create(config, auth.nobody(config), '_User', user) .then(r => { expect(Object.keys(r.response).length).toEqual(3); expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.sessionToken).toEqual('string'); return rest.find(config, auth.master(config), '_Session', { sessionToken: r.response.sessionToken, }); }) .then(r => { expect(r.results.length).toEqual(1); const session = r.results[0]; const actual = new Date(session.expiresAt.iso); const expected = new Date(now.getTime() + sessionLength * 1000); expect(Math.abs(actual - expected) <= jasmine.DEFAULT_TIMEOUT_INTERVAL).toEqual(true); done(); }) .catch(err => { jfail(err); done(); }); }); it('can create a session with no expiration', done => { const user = { username: 'asdf', password: 'zxcv', foo: 'bar', }; config.expireInactiveSessions = false; rest .create(config, auth.nobody(config), '_User', user) .then(r => { expect(Object.keys(r.response).length).toEqual(3); expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.sessionToken).toEqual('string'); return rest.find(config, auth.master(config), '_Session', { sessionToken: r.response.sessionToken, }); }) .then(r => { expect(r.results.length).toEqual(1); const session = r.results[0]; expect(session.expiresAt).toBeUndefined(); done(); }) .catch(err => { console.error(err); fail(err); done(); }); }); it('can create object in volatileClasses if masterKey', done => { rest .create(config, auth.master(config), '_PushStatus', {}) .then(r => { expect(r.response.objectId.length).toBe(10); }) .then(() => { rest.create(config, auth.master(config), '_JobStatus', {}).then(r => { expect(r.response.objectId.length).toBe(10); done(); }); }); }); it('cannot create object in volatileClasses if not masterKey', done => { Promise.resolve() .then(() => { return rest.create(config, auth.nobody(config), '_PushStatus', {}); }) .catch(error => { expect(error.code).toEqual(119); done(); }); }); it('cannot get object in volatileClasses if not masterKey through pointer', async () => { loggerErrorSpy.calls.reset(); const masterKeyOnlyClassObject = new Parse.Object('_PushStatus'); await masterKeyOnlyClassObject.save(null, { useMasterKey: true }); const obj2 = new Parse.Object('TestObject'); // Anyone is can basically create a pointer to any object // or some developers can use master key in some hook to link // private objects to standard objects obj2.set('pointer', masterKeyOnlyClassObject); await obj2.save(); const query = new Parse.Query('TestObject'); query.include('pointer'); await expectAsync(query.get(obj2.id)).toBeRejectedWithError( 'Permission denied' ); expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Clients aren't allowed to perform the get operation on the _PushStatus collection.")); }); it_id('3ce563bf-93aa-4d0b-9af9-c5fb246ac9fc')(it)('cannot get object in _GlobalConfig if not masterKey through pointer', async () => { loggerErrorSpy.calls.reset(); await Parse.Config.save({ privateData: 'secret' }, { privateData: true }); const obj2 = new Parse.Object('TestObject'); obj2.set('globalConfigPointer', { __type: 'Pointer', className: '_GlobalConfig', objectId: 1, }); await obj2.save(); const query = new Parse.Query('TestObject'); query.include('globalConfigPointer'); await expectAsync(query.get(obj2.id)).toBeRejectedWithError( 'Permission denied' ); expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Clients aren't allowed to perform the get operation on the _GlobalConfig collection.")); }); it('locks down session', done => { let currentUser; Parse.User.signUp('foo', 'bar') .then(user => { currentUser = user; const sessionToken = user.getSessionToken(); const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', 'X-Parse-Session-Token': sessionToken, }; let sessionId; return request({ headers: headers, url: 'http://localhost:8378/1/sessions/me', }) .then(response => { sessionId = response.data.objectId; return request({ headers, method: 'PUT', url: 'http://localhost:8378/1/sessions/' + sessionId, body: { installationId: 'yolo', }, }); }) .then(done.fail, res => { expect(res.status).toBe(400); expect(res.data.code).toBe(105); return request({ headers, method: 'PUT', url: 'http://localhost:8378/1/sessions/' + sessionId, body: { sessionToken: 'yolo', }, }); }) .then(done.fail, res => { expect(res.status).toBe(400); expect(res.data.code).toBe(105); return Parse.User.signUp('other', 'user'); }) .then(otherUser => { const user = new Parse.User(); user.id = otherUser.id; return request({ headers, method: 'PUT', url: 'http://localhost:8378/1/sessions/' + sessionId, body: { user: Parse._encode(user), }, }); }) .then(done.fail, res => { expect(res.status).toBe(400); expect(res.data.code).toBe(105); const user = new Parse.User(); user.id = currentUser.id; return request({ headers, method: 'PUT', url: 'http://localhost:8378/1/sessions/' + sessionId, body: { user: Parse._encode(user), }, }); }) .then(done) .catch(done.fail); }) .catch(done.fail); }); it('sets current user in new sessions', done => { let currentUser; Parse.User.signUp('foo', 'bar') .then(user => { currentUser = user; const sessionToken = user.getSessionToken(); const headers = { 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', 'X-Parse-Session-Token': sessionToken, 'Content-Type': 'application/json', }; return request({ headers, method: 'POST', url: 'http://localhost:8378/1/sessions', body: { user: { __type: 'Pointer', className: '_User', objectId: 'fakeId' }, }, }); }) .then(response => { if (response.data.user.objectId === currentUser.id) { return done(); } else { return done.fail(); } }) .catch(done.fail); }); }); describe('rest update', () => { it('ignores createdAt', done => { const config = Config.get('test'); const nobody = auth.nobody(config); const className = 'Foo'; const newCreatedAt = new Date('1970-01-01T00:00:00.000Z'); rest .create(config, nobody, className, {}) .then(res => { const objectId = res.response.objectId; const restObject = { createdAt: { __type: 'Date', iso: newCreatedAt }, // should be ignored }; return rest.update(config, nobody, className, { objectId }, restObject).then(() => { const restWhere = { objectId: objectId, }; return rest.find(config, nobody, className, restWhere, {}); }); }) .then(res2 => { const updatedObject = res2.results[0]; expect(new Date(updatedObject.createdAt)).not.toEqual(newCreatedAt); done(); }) .then(done) .catch(done.fail); }); }); describe('read-only masterKey', () => { let loggerErrorSpy; let logger; beforeEach(() => { logger = require('../lib/logger').default; loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); }); it('properly throws on rest.create, rest.update and rest.del', () => { loggerErrorSpy.calls.reset(); const config = Config.get('test'); const readOnly = auth.readOnly(config); expect(() => { rest.create(config, readOnly, 'AnObject', {}); }).toThrow( new Parse.Error( Parse.Error.OPERATION_FORBIDDEN, 'Permission denied' ) ); expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to perform the create operation.")); expect(() => { rest.update(config, readOnly, 'AnObject', {}); }).toThrow(); expect(() => { rest.del(config, readOnly, 'AnObject', {}); }).toThrow(); }); it('properly blocks writes', async () => { await reconfigureServer({ readOnlyMasterKey: 'yolo-read-only', }); // Need to be re required because reconfigureServer resets the logger const logger2 = require('../lib/logger').default; loggerErrorSpy = spyOn(logger2, 'error').and.callThrough(); try { await request({ url: `${Parse.serverURL}/classes/MyYolo`, method: 'POST', headers: { 'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-Master-Key': 'yolo-read-only', 'Content-Type': 'application/json', }, body: { foo: 'bar' }, }); fail(); } catch (res) { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe( 'Permission denied' ); expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to perform the create operation.")); } await reconfigureServer(); }); it('should throw when masterKey and readOnlyMasterKey are the same', async () => { try { await reconfigureServer({ masterKey: 'yolo', readOnlyMasterKey: 'yolo', }); fail(); } catch (err) { expect(err).toEqual(new Error('masterKey and readOnlyMasterKey should be different')); } await reconfigureServer(); }); it('should throw when masterKey and maintenanceKey are the same', async () => { await expectAsync( reconfigureServer({ masterKey: 'yolo', maintenanceKey: 'yolo', }) ).toBeRejectedWith(new Error('masterKey and maintenanceKey should be different')); }); it('should throw when trying to create RestWrite', () => { loggerErrorSpy.calls.reset(); const config = Config.get('test'); expect(() => { new RestWrite(config, auth.readOnly(config)); }).toThrow( new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied') ); expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Cannot perform a write operation when using readOnlyMasterKey")); }); it('should throw when trying to create schema', done => { loggerErrorSpy.calls.reset(); request({ method: 'POST', url: `${Parse.serverURL}/schemas`, headers: { 'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-Master-Key': 'read-only-test', 'Content-Type': 'application/json', }, json: {}, }) .then(done.fail) .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe('Permission denied'); expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to create a schema.")); done(); }); }); it('should throw when trying to create schema with a name', done => { loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/schemas/MyClass`, method: 'POST', headers: { 'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-Master-Key': 'read-only-test', 'Content-Type': 'application/json', }, json: {}, }) .then(done.fail) .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe('Permission denied'); expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to create a schema.")); done(); }); }); it('should throw when trying to update schema', done => { loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/schemas/MyClass`, method: 'PUT', headers: { 'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-Master-Key': 'read-only-test', 'Content-Type': 'application/json', }, json: {}, }) .then(done.fail) .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe('Permission denied'); expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to update a schema.")); done(); }); }); it('should throw when trying to delete schema', done => { loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/schemas/MyClass`, method: 'DELETE', headers: { 'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-Master-Key': 'read-only-test', 'Content-Type': 'application/json', }, json: {}, }) .then(done.fail) .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe('Permission denied'); expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to delete a schema.")); done(); }); }); it('should throw when trying to update the global config', done => { loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/config`, method: 'PUT', headers: { 'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-Master-Key': 'read-only-test', 'Content-Type': 'application/json', }, json: {}, }) .then(done.fail) .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe('Permission denied'); expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to update the config.")); done(); }); }); it('should throw when trying to send push', done => { loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/push`, method: 'POST', headers: { 'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-Master-Key': 'read-only-test', 'Content-Type': 'application/json', }, json: {}, }) .then(done.fail) .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe( 'Permission denied' ); expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to send push notifications.")); done(); }); }); }); describe('rest context', () => { it('should support dependency injection on rest api', async () => { const requestContextMiddleware = (req, res, next) => { req.config.aCustomController = 'aCustomController'; next(); }; let called await reconfigureServer({ requestContextMiddleware }); Parse.Cloud.beforeSave('_User', request => { expect(request.config.aCustomController).toEqual('aCustomController'); called = true; }); const user = new Parse.User(); user.setUsername('test'); user.setPassword('test'); await user.signUp(); expect(called).toBe(true); }); });