'use strict'; const Config = require('../lib/Config'); const SchemaController = require('../lib/Controllers/SchemaController'); const dd = require('deep-diff'); const TestUtils = require('../lib/TestUtils'); let config; const hasAllPODobject = () => { const obj = new Parse.Object('HasAllPOD'); obj.set('aNumber', 5); obj.set('aString', 'string'); obj.set('aBool', true); obj.set('aDate', new Date()); obj.set('aObject', { k1: 'value', k2: true, k3: 5 }); obj.set('aArray', ['contents', true, 5]); obj.set('aGeoPoint', new Parse.GeoPoint({ latitude: 0, longitude: 0 })); obj.set('aFile', new Parse.File('f.txt', { base64: 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=' })); return obj; }; describe('SchemaController', () => { beforeEach(() => { config = Config.get('test'); }); afterEach(async () => { await config.database.schemaCache.clear(); await TestUtils.destroyAllDataPermanently(false); }); it('can validate one object', done => { config.database .loadSchema() .then(schema => { return schema.validateObject('TestObject', { a: 1, b: 'yo', c: false }); }) .then( () => { done(); }, error => { jfail(error); done(); } ); }); it('can validate one object with dot notation', done => { config.database .loadSchema() .then(schema => { return schema.validateObject('TestObjectWithSubDoc', { x: false, y: 'YY', z: 1, 'aObject.k1': 'newValue', }); }) .then( () => { done(); }, error => { jfail(error); done(); } ); }); it('can validate two objects in a row', done => { config.database .loadSchema() .then(schema => { return schema.validateObject('Foo', { x: true, y: 'yyy', z: 0 }); }) .then(schema => { return schema.validateObject('Foo', { x: false, y: 'YY', z: 1 }); }) .then(() => { done(); }); }); it('can validate Relation object', done => { config.database .loadSchema() .then(schema => { return schema.validateObject('Stuff', { aRelation: { __type: 'Relation', className: 'Stuff' }, }); }) .then(schema => { return schema .validateObject('Stuff', { aRelation: { __type: 'Pointer', className: 'Stuff' }, }) .then( () => { done.fail('expected invalidity'); }, () => done() ); }, done.fail); }); it('rejects inconsistent types', done => { config.database .loadSchema() .then(schema => { return schema.validateObject('Stuff', { bacon: 7 }); }) .then(schema => { return schema.validateObject('Stuff', { bacon: 'z' }); }) .then( () => { fail('expected invalidity'); done(); }, () => done() ); }); it('updates when new fields are added', done => { config.database .loadSchema() .then(schema => { return schema.validateObject('Stuff', { bacon: 7 }); }) .then(schema => { return schema.validateObject('Stuff', { sausage: 8 }); }) .then(schema => { return schema.validateObject('Stuff', { sausage: 'ate' }); }) .then( () => { fail('expected invalidity'); done(); }, () => done() ); }); it('class-level permissions test find', done => { config.database .loadSchema() .then(schema => { // Just to create a valid class return schema.validateObject('Stuff', { foo: 'bar' }); }) .then(schema => { return schema.setPermissions('Stuff', { find: {}, }); }) .then(() => { const query = new Parse.Query('Stuff'); return query.find(); }) .then( () => { fail('Class permissions should have rejected this query.'); done(); }, () => { done(); } ); }); it('class-level permissions test user', done => { let user; createTestUser() .then(u => { user = u; return config.database.loadSchema(); }) .then(schema => { // Just to create a valid class return schema.validateObject('Stuff', { foo: 'bar' }); }) .then(schema => { const find = {}; find[user.id] = true; return schema.setPermissions('Stuff', { find: find, }); }) .then(() => { const query = new Parse.Query('Stuff'); return query.find(); }) .then( () => { done(); }, () => { fail('Class permissions should have allowed this query.'); done(); } ); }); it('class-level permissions test get', done => { let obj; createTestUser().then(user => { return ( config.database .loadSchema() // Create a valid class .then(schema => schema.validateObject('Stuff', { foo: 'bar' })) .then(schema => { const find = {}; const get = {}; get[user.id] = true; return schema.setPermissions('Stuff', { create: { '*': true }, find: find, get: get, }); }) .then(() => { obj = new Parse.Object('Stuff'); obj.set('foo', 'bar'); return obj.save(); }) .then(o => { obj = o; const query = new Parse.Query('Stuff'); return query.find(); }) .then( () => { fail('Class permissions should have rejected this query.'); done(); }, () => { const query = new Parse.Query('Stuff'); return query.get(obj.id).then( () => { done(); }, () => { fail('Class permissions should have allowed this get query'); done(); } ); } ) ); }); }); it('class-level permissions test count', done => { let obj; return ( config.database .loadSchema() // Create a valid class .then(schema => schema.validateObject('Stuff', { foo: 'bar' })) .then(schema => { const count = {}; return schema.setPermissions('Stuff', { create: { '*': true }, find: { '*': true }, count: count, }); }) .then(() => { obj = new Parse.Object('Stuff'); obj.set('foo', 'bar'); return obj.save(); }) .then(o => { obj = o; const query = new Parse.Query('Stuff'); return query.find(); }) .then(results => { expect(results.length).toBe(1); const query = new Parse.Query('Stuff'); return query.count(); }) .then( () => { fail('Class permissions should have rejected this query.'); }, err => { expect(err.message).toEqual('Permission denied for action count on class Stuff.'); done(); } ) ); }); it('can add classes without needing an object', done => { config.database .loadSchema() .then(schema => schema.addClassIfNotExists('NewClass', { foo: { type: 'String' }, }) ) .then(actualSchema => { const expectedSchema = { className: 'NewClass', fields: { objectId: { type: 'String' }, updatedAt: { type: 'Date' }, createdAt: { type: 'Date' }, ACL: { type: 'ACL' }, foo: { type: 'String' }, }, classLevelPermissions: { find: { '*': true }, get: { '*': true }, count: { '*': true }, create: { '*': true }, update: { '*': true }, delete: { '*': true }, addField: { '*': true }, protectedFields: { '*': [] }, }, }; expect(dd(actualSchema, expectedSchema)).toEqual(undefined); done(); }) .catch(error => { fail('Error creating class: ' + JSON.stringify(error)); }); }); it('can update classes without needing an object', done => { const levelPermissions = { find: { '*': true }, get: { '*': true }, count: { '*': true }, create: { '*': true }, update: { '*': true }, delete: { '*': true }, addField: { '*': true }, protectedFields: { '*': [] }, }; config.database.loadSchema().then(schema => { schema .validateObject('NewClass', { foo: 2 }) .then(() => schema.reloadData()) .then(() => schema.updateClass( 'NewClass', { fooOne: { type: 'Number' }, fooTwo: { type: 'Array' }, fooThree: { type: 'Date' }, fooFour: { type: 'Object' }, fooFive: { type: 'Relation', targetClass: '_User' }, fooSix: { type: 'String' }, fooSeven: { type: 'Object' }, fooEight: { type: 'String' }, fooNine: { type: 'String' }, fooTeen: { type: 'Number' }, fooEleven: { type: 'String' }, fooTwelve: { type: 'String' }, fooThirteen: { type: 'String' }, fooFourteen: { type: 'String' }, fooFifteen: { type: 'String' }, fooSixteen: { type: 'String' }, fooEighteen: { type: 'String' }, fooNineteen: { type: 'String' }, }, levelPermissions, {}, config.database ) ) .then(actualSchema => { const expectedSchema = { className: 'NewClass', fields: { objectId: { type: 'String' }, updatedAt: { type: 'Date' }, createdAt: { type: 'Date' }, ACL: { type: 'ACL' }, foo: { type: 'Number' }, fooOne: { type: 'Number' }, fooTwo: { type: 'Array' }, fooThree: { type: 'Date' }, fooFour: { type: 'Object' }, fooFive: { type: 'Relation', targetClass: '_User' }, fooSix: { type: 'String' }, fooSeven: { type: 'Object' }, fooEight: { type: 'String' }, fooNine: { type: 'String' }, fooTeen: { type: 'Number' }, fooEleven: { type: 'String' }, fooTwelve: { type: 'String' }, fooThirteen: { type: 'String' }, fooFourteen: { type: 'String' }, fooFifteen: { type: 'String' }, fooSixteen: { type: 'String' }, fooEighteen: { type: 'String' }, fooNineteen: { type: 'String' }, }, classLevelPermissions: { ...levelPermissions }, indexes: { _id_: { _id: 1 }, }, }; expect(dd(actualSchema, expectedSchema)).toEqual(undefined); done(); }) .catch(error => { console.trace(error); done(); fail('Error creating class: ' + JSON.stringify(error)); }); }); }); it('can update class level permission', done => { const newLevelPermissions = { find: {}, get: { '*': true }, count: {}, create: { '*': true }, update: {}, delete: { '*': true }, addField: {}, protectedFields: { '*': [] }, }; config.database.loadSchema().then(schema => { schema .validateObject('NewClass', { foo: 2 }) .then(() => schema.reloadData()) .then(() => schema.updateClass('NewClass', {}, newLevelPermissions, {}, config.database)) .then(actualSchema => { expect(dd(actualSchema.classLevelPermissions, newLevelPermissions)).toEqual(undefined); done(); }) .catch(error => { console.trace(error); done(); fail('Error creating class: ' + JSON.stringify(error)); }); }); }); it('will fail to create a class if that class was already created by an object', done => { config.database.loadSchema().then(schema => { schema .validateObject('NewClass', { foo: 7 }) .then(() => schema.reloadData()) .then(() => schema.addClassIfNotExists('NewClass', { foo: { type: 'String' }, }) ) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); expect(error.message).toEqual('Class NewClass already exists.'); done(); }); }); }); it('will resolve class creation races appropriately', done => { // If two callers race to create the same schema, the response to the // race loser should be the same as if they hadn't been racing. config.database.loadSchema().then(schema => { const p1 = schema .addClassIfNotExists('NewClass', { foo: { type: 'String' }, }) .then(validateSchema) .catch(validateError); const p2 = schema .addClassIfNotExists('NewClass', { foo: { type: 'String' }, }) .then(validateSchema) .catch(validateError); let schemaValidated = false; function validateSchema(actualSchema) { const expectedSchema = { className: 'NewClass', fields: { objectId: { type: 'String' }, updatedAt: { type: 'Date' }, createdAt: { type: 'Date' }, ACL: { type: 'ACL' }, foo: { type: 'String' }, }, classLevelPermissions: { find: { '*': true }, get: { '*': true }, count: { '*': true }, create: { '*': true }, update: { '*': true }, delete: { '*': true }, addField: { '*': true }, protectedFields: { '*': [] }, }, }; expect(dd(actualSchema, expectedSchema)).toEqual(undefined); schemaValidated = true; } let errorValidated = false; function validateError(error) { expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); expect(error.message).toEqual('Class NewClass already exists.'); errorValidated = true; } Promise.all([p1, p2]).then(() => { expect(schemaValidated).toEqual(true); expect(errorValidated).toEqual(true); done(); }); }); }); it('refuses to create classes with invalid names', done => { config.database.loadSchema().then(schema => { schema.addClassIfNotExists('_InvalidName', { foo: { type: 'String' } }).catch(error => { expect(error.message).toEqual( 'Invalid classname: _InvalidName, classnames can only have alphanumeric characters and _, and must start with an alpha character ' ); done(); }); }); }); it('refuses to add fields with invalid names', done => { config.database .loadSchema() .then(schema => schema.addClassIfNotExists('NewClass', { '0InvalidName': { type: 'String' }, }) ) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_KEY_NAME); expect(error.message).toEqual('invalid field name: 0InvalidName'); done(); }); }); it('refuses to explicitly create the default fields for custom classes', done => { config.database .loadSchema() .then(schema => schema.addClassIfNotExists('NewClass', { objectId: { type: 'String' } })) .catch(error => { expect(error.code).toEqual(136); expect(error.message).toEqual('field objectId cannot be added'); done(); }); }); it('refuses to explicitly create the default fields for non-custom classes', done => { config.database .loadSchema() .then(schema => schema.addClassIfNotExists('_Installation', { localeIdentifier: { type: 'String' }, }) ) .catch(error => { expect(error.code).toEqual(136); expect(error.message).toEqual('field localeIdentifier cannot be added'); done(); }); }); it('refuses to add fields with invalid types', done => { config.database .loadSchema() .then(schema => schema.addClassIfNotExists('NewClass', { foo: { type: 7 }, }) ) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_JSON); expect(error.message).toEqual('invalid JSON'); done(); }); }); it('refuses to add fields with invalid pointer types', done => { config.database .loadSchema() .then(schema => schema.addClassIfNotExists('NewClass', { foo: { type: 'Pointer' }, }) ) .catch(error => { expect(error.code).toEqual(135); expect(error.message).toEqual('type Pointer needs a class name'); done(); }); }); it('refuses to add fields with invalid pointer target', done => { config.database .loadSchema() .then(schema => schema.addClassIfNotExists('NewClass', { foo: { type: 'Pointer', targetClass: 7 }, }) ) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_JSON); expect(error.message).toEqual('invalid JSON'); done(); }); }); it('refuses to add fields with invalid Relation type', done => { config.database .loadSchema() .then(schema => schema.addClassIfNotExists('NewClass', { foo: { type: 'Relation', uselessKey: 7 }, }) ) .catch(error => { expect(error.code).toEqual(135); expect(error.message).toEqual('type Relation needs a class name'); done(); }); }); it('refuses to add fields with invalid relation target', done => { config.database .loadSchema() .then(schema => schema.addClassIfNotExists('NewClass', { foo: { type: 'Relation', targetClass: 7 }, }) ) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_JSON); expect(error.message).toEqual('invalid JSON'); done(); }); }); it('refuses to add fields with uncreatable pointer target class', done => { config.database .loadSchema() .then(schema => schema.addClassIfNotExists('NewClass', { foo: { type: 'Pointer', targetClass: 'not a valid class name' }, }) ) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); expect(error.message).toEqual( 'Invalid classname: not a valid class name, classnames can only have alphanumeric characters and _, and must start with an alpha character ' ); done(); }); }); it('refuses to add fields with uncreatable relation target class', done => { config.database .loadSchema() .then(schema => schema.addClassIfNotExists('NewClass', { foo: { type: 'Relation', targetClass: 'not a valid class name' }, }) ) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); expect(error.message).toEqual( 'Invalid classname: not a valid class name, classnames can only have alphanumeric characters and _, and must start with an alpha character ' ); done(); }); }); it('refuses to add fields with unknown types', done => { config.database .loadSchema() .then(schema => schema.addClassIfNotExists('NewClass', { foo: { type: 'Unknown' }, }) ) .catch(error => { expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE); expect(error.message).toEqual('invalid field type: Unknown'); done(); }); }); it('refuses to add CLP with incorrect find', done => { const levelPermissions = { find: { '*': false }, get: { '*': true }, create: { '*': true }, update: { '*': true }, delete: { '*': true }, addField: { '*': true }, protectedFields: { '*': ['email'] }, }; config.database.loadSchema().then(schema => { schema .validateObject('NewClass', {}) .then(() => schema.reloadData()) .then(() => schema.updateClass('NewClass', {}, levelPermissions, {}, config.database)) .then(done.fail) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_JSON); done(); }); }); }); it('refuses to add CLP when incorrectly sending a string to protectedFields object value instead of an array', done => { const levelPermissions = { find: { '*': true }, get: { '*': true }, create: { '*': true }, update: { '*': true }, delete: { '*': true }, addField: { '*': true }, protectedFields: { '*': 'email' }, }; config.database.loadSchema().then(schema => { schema .validateObject('NewClass', {}) .then(() => schema.reloadData()) .then(() => schema.updateClass('NewClass', {}, levelPermissions, {}, config.database)) .then(done.fail) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_JSON); done(); }); }); }); it('will create classes', done => { config.database .loadSchema() .then(schema => schema.addClassIfNotExists('NewClass', { aNumber: { type: 'Number' }, aString: { type: 'String' }, aBool: { type: 'Boolean' }, aDate: { type: 'Date' }, aObject: { type: 'Object' }, aArray: { type: 'Array' }, aGeoPoint: { type: 'GeoPoint' }, aFile: { type: 'File' }, aPointer: { type: 'Pointer', targetClass: 'ThisClassDoesNotExistYet', }, aRelation: { type: 'Relation', targetClass: 'NewClass' }, aBytes: { type: 'Bytes' }, aPolygon: { type: 'Polygon' }, }) ) .then(actualSchema => { const expectedSchema = { className: 'NewClass', fields: { objectId: { type: 'String' }, updatedAt: { type: 'Date' }, createdAt: { type: 'Date' }, ACL: { type: 'ACL' }, aString: { type: 'String' }, aNumber: { type: 'Number' }, aBool: { type: 'Boolean' }, aDate: { type: 'Date' }, aObject: { type: 'Object' }, aArray: { type: 'Array' }, aGeoPoint: { type: 'GeoPoint' }, aFile: { type: 'File' }, aPointer: { type: 'Pointer', targetClass: 'ThisClassDoesNotExistYet', }, aRelation: { type: 'Relation', targetClass: 'NewClass' }, aBytes: { type: 'Bytes' }, aPolygon: { type: 'Polygon' }, }, classLevelPermissions: { find: { '*': true }, get: { '*': true }, count: { '*': true }, create: { '*': true }, update: { '*': true }, delete: { '*': true }, addField: { '*': true }, protectedFields: { '*': [] }, }, }; expect(dd(actualSchema, expectedSchema)).toEqual(undefined); done(); }); }); it('creates the default fields for non-custom classes', done => { config.database .loadSchema() .then(schema => schema.addClassIfNotExists('_Installation', { foo: { type: 'Number' }, }) ) .then(actualSchema => { const expectedSchema = { className: '_Installation', fields: { objectId: { type: 'String' }, updatedAt: { type: 'Date' }, createdAt: { type: 'Date' }, ACL: { type: 'ACL' }, foo: { type: 'Number' }, installationId: { type: 'String' }, deviceToken: { type: 'String' }, channels: { type: 'Array' }, deviceType: { type: 'String' }, pushType: { type: 'String' }, GCMSenderId: { type: 'String' }, timeZone: { type: 'String' }, localeIdentifier: { type: 'String' }, badge: { type: 'Number' }, appVersion: { type: 'String' }, appName: { type: 'String' }, appIdentifier: { type: 'String' }, parseVersion: { type: 'String' }, }, classLevelPermissions: { find: { '*': true }, get: { '*': true }, count: { '*': true }, create: { '*': true }, update: { '*': true }, delete: { '*': true }, addField: { '*': true }, protectedFields: { '*': [] }, }, }; expect(dd(actualSchema, expectedSchema)).toEqual(undefined); done(); }); }); it('creates non-custom classes which include relation field', done => { config.database .loadSchema() //as `_Role` is always created by default, we only get it here .then(schema => schema.getOneSchema('_Role')) .then(actualSchema => { const expectedSchema = { className: '_Role', fields: { objectId: { type: 'String' }, updatedAt: { type: 'Date' }, createdAt: { type: 'Date' }, ACL: { type: 'ACL' }, name: { type: 'String' }, users: { type: 'Relation', targetClass: '_User' }, roles: { type: 'Relation', targetClass: '_Role' }, }, classLevelPermissions: { find: { '*': true }, get: { '*': true }, count: { '*': true }, create: { '*': true }, update: { '*': true }, delete: { '*': true }, addField: { '*': true }, protectedFields: { '*': [] }, }, }; expect(dd(actualSchema, expectedSchema)).toEqual(undefined); done(); }); }); it('creates non-custom classes which include pointer field', done => { config.database .loadSchema() .then(schema => schema.addClassIfNotExists('_Session', {})) .then(actualSchema => { const expectedSchema = { className: '_Session', fields: { objectId: { type: 'String' }, updatedAt: { type: 'Date' }, createdAt: { type: 'Date' }, restricted: { type: 'Boolean' }, user: { type: 'Pointer', targetClass: '_User' }, installationId: { type: 'String' }, sessionToken: { type: 'String' }, expiresAt: { type: 'Date' }, createdWith: { type: 'Object' }, ACL: { type: 'ACL' }, }, classLevelPermissions: { find: { '*': true }, get: { '*': true }, count: { '*': true }, create: { '*': true }, update: { '*': true }, delete: { '*': true }, addField: { '*': true }, protectedFields: { '*': [] }, }, }; expect(dd(actualSchema, expectedSchema)).toEqual(undefined); done(); }); }); it('refuses to create two geopoints', done => { config.database .loadSchema() .then(schema => schema.addClassIfNotExists('NewClass', { geo1: { type: 'GeoPoint' }, geo2: { type: 'GeoPoint' }, }) ) .catch(error => { expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE); expect(error.message).toEqual( 'currently, only one GeoPoint field may exist in an object. Adding geo2 when geo1 already exists.' ); done(); }); }); it('can check if a class exists', done => { config.database .loadSchema() .then(schema => { return schema .addClassIfNotExists('NewClass', {}) .then(() => schema.reloadData({ clearCache: true })) .then(() => { schema .hasClass('NewClass') .then(hasClass => { expect(hasClass).toEqual(true); done(); }) .catch(fail); schema .hasClass('NonexistantClass') .then(hasClass => { expect(hasClass).toEqual(false); done(); }) .catch(fail); }) .catch(error => { fail("Couldn't create class"); jfail(error); }); }) .catch(() => fail("Couldn't load schema")); }); it('refuses to delete fields from invalid class names', done => { config.database .loadSchema() .then(schema => schema.deleteField('fieldName', 'invalid class name')) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); done(); }); }); it('refuses to delete invalid fields', done => { config.database .loadSchema() .then(schema => schema.deleteField('invalid field name', 'ValidClassName')) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_KEY_NAME); done(); }); }); it('refuses to delete the default fields', done => { config.database .loadSchema() .then(schema => schema.deleteField('installationId', '_Installation')) .catch(error => { expect(error.code).toEqual(136); expect(error.message).toEqual('field installationId cannot be changed'); done(); }); }); it('refuses to delete fields from nonexistant classes', done => { config.database .loadSchema() .then(schema => schema.deleteField('field', 'NoClass')) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); expect(error.message).toEqual('Class NoClass does not exist.'); done(); }); }); it('refuses to delete fields that dont exist', done => { hasAllPODobject() .save() .then(() => config.database.loadSchema()) .then(schema => schema.deleteField('missingField', 'HasAllPOD')) .catch(error => { expect(error.code).toEqual(255); expect(error.message).toEqual('Field missingField does not exist, cannot delete.'); done(); }); }); it('drops related collection when deleting relation field', done => { const obj1 = hasAllPODobject(); obj1 .save() .then(savedObj1 => { const obj2 = new Parse.Object('HasPointersAndRelations'); obj2.set('aPointer', savedObj1); const relation = obj2.relation('aRelation'); relation.add(obj1); return obj2.save(); }) .then(() => config.database.collectionExists('_Join:aRelation:HasPointersAndRelations')) .then(exists => { if (!exists) { fail('Relation collection ' + 'should exist after save.'); } }) .then(() => config.database.loadSchema()) .then(schema => schema.deleteField('aRelation', 'HasPointersAndRelations', config.database)) .then(() => config.database.collectionExists('_Join:aRelation:HasPointersAndRelations')) .then( exists => { if (exists) { fail('Relation collection should not exist after deleting relation field.'); } done(); }, error => { jfail(error); done(); } ); }); it('can delete relation field when related _Join collection not exist', done => { config.database.loadSchema().then(schema => { schema .addClassIfNotExists('NewClass', { relationField: { type: 'Relation', targetClass: '_User' }, }) .then(actualSchema => { const expectedSchema = { className: 'NewClass', fields: { objectId: { type: 'String' }, updatedAt: { type: 'Date' }, createdAt: { type: 'Date' }, ACL: { type: 'ACL' }, relationField: { type: 'Relation', targetClass: '_User' }, }, classLevelPermissions: { find: { '*': true }, get: { '*': true }, count: { '*': true }, create: { '*': true }, update: { '*': true }, delete: { '*': true }, addField: { '*': true }, protectedFields: { '*': [] }, }, }; expect(dd(actualSchema, expectedSchema)).toEqual(undefined); }) .then(() => config.database.collectionExists('_Join:relationField:NewClass')) .then(exist => { on_db( 'postgres', () => { // We create the table when creating the column expect(exist).toEqual(true); }, () => { expect(exist).toEqual(false); } ); }) .then(() => schema.deleteField('relationField', 'NewClass', config.database)) .then(() => schema.reloadData()) .then(() => { const expectedSchema = { objectId: { type: 'String' }, updatedAt: { type: 'Date' }, createdAt: { type: 'Date' }, ACL: { type: 'ACL' }, }; expect(dd(schema.schemaData.NewClass.fields, expectedSchema)).toEqual(undefined); }) .then(done) .catch(done.fail); }); }); it('can delete string fields and resave as number field', done => { Parse.Object.disableSingleInstance(); const obj1 = hasAllPODobject(); const obj2 = hasAllPODobject(); Parse.Object.saveAll([obj1, obj2]) .then(() => config.database.loadSchema()) .then(schema => schema.deleteField('aString', 'HasAllPOD', config.database)) .then(() => new Parse.Query('HasAllPOD').get(obj1.id)) .then(obj1Reloaded => { expect(obj1Reloaded.get('aString')).toEqual(undefined); obj1Reloaded.set('aString', ['not a string', 'this time']); obj1Reloaded .save() .then(obj1reloadedAgain => { expect(obj1reloadedAgain.get('aString')).toEqual(['not a string', 'this time']); return new Parse.Query('HasAllPOD').get(obj2.id); }) .then(obj2reloaded => { expect(obj2reloaded.get('aString')).toEqual(undefined); done(); }); }) .catch(error => { jfail(error); done(); }); }); it('can delete pointer fields and resave as string', done => { Parse.Object.disableSingleInstance(); const obj1 = new Parse.Object('NewClass'); obj1 .save() .then(() => { obj1.set('aPointer', obj1); return obj1.save(); }) .then(obj1 => { expect(obj1.get('aPointer').id).toEqual(obj1.id); }) .then(() => config.database.loadSchema()) .then(schema => schema.deleteField('aPointer', 'NewClass', config.database)) .then(() => new Parse.Query('NewClass').get(obj1.id)) .then(obj1 => { expect(obj1.get('aPointer')).toEqual(undefined); obj1.set('aPointer', 'Now a string'); return obj1.save(); }) .then(obj1 => { expect(obj1.get('aPointer')).toEqual('Now a string'); done(); }); }); it('can merge schemas', done => { expect( SchemaController.buildMergedSchemaObject( { _id: 'SomeClass', someType: { type: 'Number' }, }, { newType: { type: 'Number' }, } ) ).toEqual({ someType: { type: 'Number' }, newType: { type: 'Number' }, }); done(); }); it('can merge deletions', done => { expect( SchemaController.buildMergedSchemaObject( { _id: 'SomeClass', someType: { type: 'Number' }, outDatedType: { type: 'String' }, }, { newType: { type: 'GeoPoint' }, outDatedType: { __op: 'Delete' }, } ) ).toEqual({ someType: { type: 'Number' }, newType: { type: 'GeoPoint' }, }); done(); }); it('ignore default field when merge with system class', done => { expect( SchemaController.buildMergedSchemaObject( { _id: '_User', username: { type: 'String' }, password: { type: 'String' }, email: { type: 'String' }, emailVerified: { type: 'Boolean' }, }, { emailVerified: { type: 'String' }, customField: { type: 'String' }, } ) ).toEqual({ customField: { type: 'String' }, }); done(); }); it('yields a proper schema mismatch error (#2661)', done => { const anObject = new Parse.Object('AnObject'); const anotherObject = new Parse.Object('AnotherObject'); const someObject = new Parse.Object('SomeObject'); Parse.Object.saveAll([anObject, anotherObject, someObject]) .then(() => { anObject.set('pointer', anotherObject); return anObject.save(); }) .then(() => { anObject.set('pointer', someObject); return anObject.save(); }) .then( () => { fail('shoud not save correctly'); done(); }, err => { expect(err instanceof Parse.Error).toBeTruthy(); expect(err.message).toEqual( 'schema mismatch for AnObject.pointer; expected Pointer but got Pointer' ); done(); } ); }); it('yields a proper schema mismatch error bis (#2661)', done => { const anObject = new Parse.Object('AnObject'); const someObject = new Parse.Object('SomeObject'); Parse.Object.saveAll([anObject, someObject]) .then(() => { anObject.set('number', 1); return anObject.save(); }) .then(() => { anObject.set('number', someObject); return anObject.save(); }) .then( () => { fail('shoud not save correctly'); done(); }, err => { expect(err instanceof Parse.Error).toBeTruthy(); expect(err.message).toEqual( 'schema mismatch for AnObject.number; expected Number but got Pointer' ); done(); } ); }); it('yields a proper schema mismatch error ter (#2661)', done => { const anObject = new Parse.Object('AnObject'); const someObject = new Parse.Object('SomeObject'); Parse.Object.saveAll([anObject, someObject]) .then(() => { anObject.set('pointer', someObject); return anObject.save(); }) .then(() => { anObject.set('pointer', 1); return anObject.save(); }) .then( () => { fail('shoud not save correctly'); done(); }, err => { expect(err instanceof Parse.Error).toBeTruthy(); expect(err.message).toEqual( 'schema mismatch for AnObject.pointer; expected Pointer but got Number' ); done(); } ); }); it('properly handles volatile _Schemas', done => { function validateSchemaStructure(schema) { expect(Object.prototype.hasOwnProperty.call(schema, 'className')).toBe(true); expect(Object.prototype.hasOwnProperty.call(schema, 'fields')).toBe(true); expect(Object.prototype.hasOwnProperty.call(schema, 'classLevelPermissions')).toBe(true); } function validateSchemaDataStructure(schemaData) { Object.keys(schemaData).forEach(className => { const schema = schemaData[className]; // Hooks has className... if (className != '_Hooks') { expect(Object.prototype.hasOwnProperty.call(schema, 'className')).toBe(false); } expect(Object.prototype.hasOwnProperty.call(schema, 'fields')).toBe(false); expect(Object.prototype.hasOwnProperty.call(schema, 'classLevelPermissions')).toBe(false); }); } let schema; config.database .loadSchema() .then(s => { schema = s; return schema.getOneSchema('_User', false); }) .then(userSchema => { validateSchemaStructure(userSchema); validateSchemaDataStructure(schema.schemaData); return schema.getOneSchema('_PushStatus', true); }) .then(pushStatusSchema => { validateSchemaStructure(pushStatusSchema); validateSchemaDataStructure(schema.schemaData); }) .then(done) .catch(done.fail); }); it('setAllClasses return classes if cache fails', async () => { const schema = await config.database.loadSchema(); spyOn(schema._cache, 'setAllClasses').and.callFake(() => Promise.reject('Oops!')); const errorSpy = spyOn(console, 'error').and.callFake(() => {}); const allSchema = await schema.setAllClasses(); expect(allSchema).toBeDefined(); expect(errorSpy).toHaveBeenCalledWith('Error saving schema to cache:', 'Oops!'); }); it('should not throw on null field types', async () => { const schema = await config.database.loadSchema(); const result = await schema.enforceFieldExists('NewClass', 'fieldName', null); expect(result).toBeUndefined(); }); it('ensureFields should throw when schema is not set', async () => { const schema = await config.database.loadSchema(); try { schema.ensureFields([ { className: 'NewClass', fieldName: 'fieldName', type: 'String', }, ]); } catch (e) { expect(e.message).toBe('Could not add field fieldName'); } }); }); describe('Class Level Permissions for requiredAuth', () => { beforeEach(() => { config = Config.get('test'); }); function createUser() { const user = new Parse.User(); user.set('username', 'hello'); user.set('password', 'world'); return user.signUp(null); } it('required auth test find', done => { config.database .loadSchema() .then(schema => { // Just to create a valid class return schema.validateObject('Stuff', { foo: 'bar' }); }) .then(schema => { return schema.setPermissions('Stuff', { find: { requiresAuthentication: true, }, }); }) .then(() => { const query = new Parse.Query('Stuff'); return query.find(); }) .then( () => { fail('Class permissions should have rejected this query.'); done(); }, e => { expect(e.message).toEqual('Permission denied, user needs to be authenticated.'); done(); } ); }); it('required auth test find authenticated', done => { config.database .loadSchema() .then(schema => { // Just to create a valid class return schema.validateObject('Stuff', { foo: 'bar' }); }) .then(schema => { return schema.setPermissions('Stuff', { find: { requiresAuthentication: true, }, }); }) .then(() => { return createUser(); }) .then(() => { const query = new Parse.Query('Stuff'); return query.find(); }) .then( results => { expect(results.length).toEqual(0); done(); }, e => { console.error(e); fail('Should not have failed'); done(); } ); }); it('required auth should allow create authenticated', done => { config.database .loadSchema() .then(schema => { // Just to create a valid class return schema.validateObject('Stuff', { foo: 'bar' }); }) .then(schema => { return schema.setPermissions('Stuff', { create: { requiresAuthentication: true, }, }); }) .then(() => { return createUser(); }) .then(() => { const stuff = new Parse.Object('Stuff'); stuff.set('foo', 'bar'); return stuff.save(); }) .then( () => { done(); }, e => { console.error(e); fail('Should not have failed'); done(); } ); }); it('required auth should reject create when not authenticated', done => { config.database .loadSchema() .then(schema => { // Just to create a valid class return schema.validateObject('Stuff', { foo: 'bar' }); }) .then(schema => { return schema.setPermissions('Stuff', { create: { requiresAuthentication: true, }, }); }) .then(() => { const stuff = new Parse.Object('Stuff'); stuff.set('foo', 'bar'); return stuff.save(); }) .then( () => { fail('Class permissions should have rejected this query.'); done(); }, e => { expect(e.message).toEqual('Permission denied, user needs to be authenticated.'); done(); } ); }); it('required auth test create/get/update/delete authenticated', done => { config.database .loadSchema() .then(schema => { // Just to create a valid class return schema.validateObject('Stuff', { foo: 'bar' }); }) .then(schema => { return schema.setPermissions('Stuff', { create: { requiresAuthentication: true, }, get: { requiresAuthentication: true, }, delete: { requiresAuthentication: true, }, update: { requiresAuthentication: true, }, }); }) .then(() => { return createUser(); }) .then(() => { const stuff = new Parse.Object('Stuff'); stuff.set('foo', 'bar'); return stuff.save().then(() => { const query = new Parse.Query('Stuff'); return query.get(stuff.id); }); }) .then(gotStuff => { return gotStuff.save({ foo: 'baz' }).then(() => { return gotStuff.destroy(); }); }) .then( () => { done(); }, e => { console.error(e); fail('Should not have failed'); done(); } ); }); it('required auth test get not authenticated', done => { config.database .loadSchema() .then(schema => { // Just to create a valid class return schema.validateObject('Stuff', { foo: 'bar' }); }) .then(schema => { return schema.setPermissions('Stuff', { get: { requiresAuthentication: true, }, create: { '*': true, }, }); }) .then(() => { const stuff = new Parse.Object('Stuff'); stuff.set('foo', 'bar'); return stuff.save().then(() => { const query = new Parse.Query('Stuff'); return query.get(stuff.id); }); }) .then( () => { fail('Should not succeed!'); done(); }, e => { expect(e.message).toEqual('Permission denied, user needs to be authenticated.'); done(); } ); }); it('required auth test find not authenticated', done => { config.database .loadSchema() .then(schema => { // Just to create a valid class return schema.validateObject('Stuff', { foo: 'bar' }); }) .then(schema => { return schema.setPermissions('Stuff', { find: { requiresAuthentication: true, }, create: { '*': true, }, get: { '*': true, }, }); }) .then(() => { const stuff = new Parse.Object('Stuff'); stuff.set('foo', 'bar'); return stuff.save().then(() => { const query = new Parse.Query('Stuff'); return query.get(stuff.id); }); }) .then(result => { expect(result.get('foo')).toEqual('bar'); const query = new Parse.Query('Stuff'); return query.find(); }) .then( () => { fail('Should not succeed!'); done(); }, e => { expect(e.message).toEqual('Permission denied, user needs to be authenticated.'); done(); } ); }); it('required auth test create/get/update/delete with roles (#3753)', done => { let user; config.database .loadSchema() .then(schema => { // Just to create a valid class return schema.validateObject('Stuff', { foo: 'bar' }); }) .then(schema => { return schema.setPermissions('Stuff', { find: { requiresAuthentication: true, 'role:admin': true, }, create: { 'role:admin': true }, update: { 'role:admin': true }, delete: { 'role:admin': true }, get: { requiresAuthentication: true, 'role:admin': true, }, }); }) .then(() => { const stuff = new Parse.Object('Stuff'); stuff.set('foo', 'bar'); return stuff .save(null, { useMasterKey: true }) .then(() => { const query = new Parse.Query('Stuff'); return query .get(stuff.id) .then( () => { done.fail('should not succeed'); }, () => { return new Parse.Query('Stuff').find(); } ) .then( () => { done.fail('should not succeed'); }, () => { return Promise.resolve(); } ); }) .then(() => { return Parse.User.signUp('user', 'password').then(signedUpUser => { user = signedUpUser; const query = new Parse.Query('Stuff'); return query.get(stuff.id, { sessionToken: user.getSessionToken(), }); }); }); }) .then(result => { expect(result.get('foo')).toEqual('bar'); const query = new Parse.Query('Stuff'); return query.find({ sessionToken: user.getSessionToken() }); }) .then( results => { expect(results.length).toBe(1); done(); }, e => { console.error(e); done.fail(e); } ); }); });