Files
kami-parse-server/spec/schemas.spec.js
2016-12-07 18:17:05 -05:00

1699 lines
48 KiB
JavaScript

'use strict';
var Parse = require('parse/node').Parse;
var request = require('request');
var dd = require('deep-diff');
var Config = require('../src/Config');
var config = new Config('test');
var hasAllPODobject = () => {
var 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=' }));
var objACL = new Parse.ACL();
objACL.setPublicWriteAccess(false);
obj.setACL(objACL);
return obj;
};
const defaultClassLevelPermissions = {
find: {
'*': true
},
create: {
'*': true
},
get: {
'*': true
},
update: {
'*': true
},
addField: {
'*': true
},
delete: {
'*': true
}
}
var plainOldDataSchema = {
className: 'HasAllPOD',
fields: {
//Default fields
ACL: {type: 'ACL'},
createdAt: {type: 'Date'},
updatedAt: {type: 'Date'},
objectId: {type: 'String'},
//Custom fields
aNumber: {type: 'Number'},
aString: {type: 'String'},
aBool: {type: 'Boolean'},
aDate: {type: 'Date'},
aObject: {type: 'Object'},
aArray: {type: 'Array'},
aGeoPoint: {type: 'GeoPoint'},
aFile: {type: 'File'}
},
classLevelPermissions: defaultClassLevelPermissions
};
var pointersAndRelationsSchema = {
className: 'HasPointersAndRelations',
fields: {
//Default fields
ACL: {type: 'ACL'},
createdAt: {type: 'Date'},
updatedAt: {type: 'Date'},
objectId: {type: 'String'},
//Custom fields
aPointer: {
type: 'Pointer',
targetClass: 'HasAllPOD',
},
aRelation: {
type: 'Relation',
targetClass: 'HasAllPOD',
},
},
classLevelPermissions: defaultClassLevelPermissions
}
const userSchema = {
"className": "_User",
"fields": {
"objectId": {"type": "String"},
"createdAt": {"type": "Date"},
"updatedAt": {"type": "Date"},
"ACL": {"type": "ACL"},
"username": {"type": "String"},
"password": {"type": "String"},
"email": {"type": "String"},
"emailVerified": {"type": "Boolean"},
"authData": {"type": "Object"}
},
"classLevelPermissions": defaultClassLevelPermissions,
}
var noAuthHeaders = {
'X-Parse-Application-Id': 'test',
};
var restKeyHeaders = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
var masterKeyHeaders = {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
};
describe('schemas', () => {
beforeEach(() => {
config.database.schemaCache.clear();
});
it('requires the master key to get all schemas', (done) => {
request.get({
url: 'http://localhost:8378/1/schemas',
json: true,
headers: noAuthHeaders,
}, (error, response, body) => {
//api.parse.com uses status code 401, but due to the lack of keys
//being necessary in parse-server, 403 makes more sense
expect(response.statusCode).toEqual(403);
expect(body.error).toEqual('unauthorized');
done();
});
});
it('requires the master key to get one schema', (done) => {
request.get({
url: 'http://localhost:8378/1/schemas/SomeSchema',
json: true,
headers: restKeyHeaders,
}, (error, response, body) => {
expect(response.statusCode).toEqual(403);
expect(body.error).toEqual('unauthorized: master key is required');
done();
});
});
it('asks for the master key if you use the rest key', (done) => {
request.get({
url: 'http://localhost:8378/1/schemas',
json: true,
headers: restKeyHeaders,
}, (error, response, body) => {
expect(response.statusCode).toEqual(403);
expect(body.error).toEqual('unauthorized: master key is required');
done();
});
});
it('creates _User schema when server starts', done => {
request.get({
url: 'http://localhost:8378/1/schemas',
json: true,
headers: masterKeyHeaders,
}, (error, response, body) => {
expect(dd(body.results, [userSchema])).toEqual();
done();
});
});
it('responds with a list of schemas after creating objects', done => {
var obj1 = hasAllPODobject();
obj1.save().then(savedObj1 => {
var obj2 = new Parse.Object('HasPointersAndRelations');
obj2.set('aPointer', savedObj1);
var relation = obj2.relation('aRelation');
relation.add(obj1);
return obj2.save();
}).then(() => {
request.get({
url: 'http://localhost:8378/1/schemas',
json: true,
headers: masterKeyHeaders,
}, (error, response, body) => {
var expected = {
results: [userSchema,plainOldDataSchema,pointersAndRelationsSchema]
};
expect(dd(body, expected)).toEqual(undefined);
done();
})
});
});
it('responds with a single schema', done => {
var obj = hasAllPODobject();
obj.save().then(() => {
request.get({
url: 'http://localhost:8378/1/schemas/HasAllPOD',
json: true,
headers: masterKeyHeaders,
}, (error, response, body) => {
expect(body).toEqual(plainOldDataSchema);
done();
});
});
});
it('treats class names case sensitively', done => {
var obj = hasAllPODobject();
obj.save().then(() => {
request.get({
url: 'http://localhost:8378/1/schemas/HASALLPOD',
json: true,
headers: masterKeyHeaders,
}, (error, response, body) => {
expect(response.statusCode).toEqual(400);
expect(body).toEqual({
code: 103,
error: 'Class HASALLPOD does not exist.',
});
done();
});
});
});
it('requires the master key to create a schema', done => {
request.post({
url: 'http://localhost:8378/1/schemas',
json: true,
headers: noAuthHeaders,
body: {
className: 'MyClass',
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(403);
expect(body.error).toEqual('unauthorized');
done();
});
});
it('asks for the master key if you use the rest key', done => {
request.post({
url: 'http://localhost:8378/1/schemas',
json: true,
headers: restKeyHeaders,
body: {
className: 'MyClass',
},
}, (error, response, body) => {
expect(response.statusCode).toEqual(403);
expect(body.error).toEqual('unauthorized: master key is required');
done();
});
});
it('sends an error if you use mismatching class names', done => {
request.post({
url: 'http://localhost:8378/1/schemas/A',
headers: masterKeyHeaders,
json: true,
body: {
className: 'B',
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(400);
expect(body).toEqual({
code: Parse.Error.INVALID_CLASS_NAME,
error: 'Class name mismatch between B and A.',
});
done();
});
});
it('sends an error if you use no class name', done => {
request.post({
url: 'http://localhost:8378/1/schemas',
headers: masterKeyHeaders,
json: true,
body: {},
}, (error, response, body) => {
expect(response.statusCode).toEqual(400);
expect(body).toEqual({
code: 135,
error: 'POST /schemas needs a class name.',
});
done();
})
});
it('sends an error if you try to create the same class twice', done => {
request.post({
url: 'http://localhost:8378/1/schemas',
headers: masterKeyHeaders,
json: true,
body: {
className: 'A',
},
}, (error) => {
expect(error).toEqual(null);
request.post({
url: 'http://localhost:8378/1/schemas',
headers: masterKeyHeaders,
json: true,
body: {
className: 'A',
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(400);
expect(body).toEqual({
code: Parse.Error.INVALID_CLASS_NAME,
error: 'Class A already exists.'
});
done();
});
});
});
it('responds with all fields when you create a class', done => {
request.post({
url: 'http://localhost:8378/1/schemas',
headers: masterKeyHeaders,
json: true,
body: {
className: "NewClass",
fields: {
foo: {type: 'Number'},
ptr: {type: 'Pointer', targetClass: 'SomeClass'}
}
}
}, (error, response, body) => {
expect(body).toEqual({
className: 'NewClass',
fields: {
ACL: {type: 'ACL'},
createdAt: {type: 'Date'},
updatedAt: {type: 'Date'},
objectId: {type: 'String'},
foo: {type: 'Number'},
ptr: {type: 'Pointer', targetClass: 'SomeClass'},
},
classLevelPermissions: defaultClassLevelPermissions
});
done();
});
});
it('responds with all fields when getting incomplete schema', done => {
config.database.loadSchema()
.then(schemaController => schemaController.addClassIfNotExists('_Installation', {}, defaultClassLevelPermissions))
.then(() => {
request.get({
url: 'http://localhost:8378/1/schemas/_Installation',
headers: masterKeyHeaders,
json: true
}, (error, response, body) => {
expect(dd(body,{
className: '_Installation',
fields: {
objectId: {type: 'String'},
updatedAt: {type: 'Date'},
createdAt: {type: 'Date'},
installationId: {type: 'String'},
deviceToken: {type: 'String'},
channels: {type: 'Array'},
deviceType: {type: 'String'},
pushType: {type: 'String'},
GCMSenderId: {type: 'String'},
timeZone: {type: 'String'},
badge: {type: 'Number'},
appIdentifier: {type: 'String'},
localeIdentifier: {type: 'String'},
appVersion: {type: 'String'},
appName: {type: 'String'},
parseVersion: {type: 'String'},
ACL: {type: 'ACL'}
},
classLevelPermissions: defaultClassLevelPermissions
})).toBeUndefined();
done();
});
})
.catch(error => {
fail(JSON.stringify(error))
done();
});
});
it('lets you specify class name in both places', done => {
request.post({
url: 'http://localhost:8378/1/schemas/NewClass',
headers: masterKeyHeaders,
json: true,
body: {
className: "NewClass",
}
}, (error, response, body) => {
expect(body).toEqual({
className: 'NewClass',
fields: {
ACL: {type: 'ACL'},
createdAt: {type: 'Date'},
updatedAt: {type: 'Date'},
objectId: {type: 'String'},
},
classLevelPermissions: defaultClassLevelPermissions
});
done();
});
});
it('requires the master key to modify schemas', done => {
request.post({
url: 'http://localhost:8378/1/schemas/NewClass',
headers: masterKeyHeaders,
json: true,
body: {},
}, () => {
request.put({
url: 'http://localhost:8378/1/schemas/NewClass',
headers: noAuthHeaders,
json: true,
body: {},
}, (error, response, body) => {
expect(response.statusCode).toEqual(403);
expect(body.error).toEqual('unauthorized');
done();
});
});
});
it('rejects class name mis-matches in put', done => {
request.put({
url: 'http://localhost:8378/1/schemas/NewClass',
headers: masterKeyHeaders,
json: true,
body: {className: 'WrongClassName'}
}, (error, response, body) => {
expect(response.statusCode).toEqual(400);
expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
expect(body.error).toEqual('Class name mismatch between WrongClassName and NewClass.');
done();
});
});
it('refuses to add fields to non-existent classes', done => {
request.put({
url: 'http://localhost:8378/1/schemas/NoClass',
headers: masterKeyHeaders,
json: true,
body: {
fields: {
newField: {type: 'String'}
}
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(400);
expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
expect(body.error).toEqual('Class NoClass does not exist.');
done();
});
});
it('refuses to put to existing fields, even if it would not be a change', done => {
var obj = hasAllPODobject();
obj.save()
.then(() => {
request.put({
url: 'http://localhost:8378/1/schemas/HasAllPOD',
headers: masterKeyHeaders,
json: true,
body: {
fields: {
aString: {type: 'String'}
}
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(400);
expect(body.code).toEqual(255);
expect(body.error).toEqual('Field aString exists, cannot update.');
done();
});
})
});
it('refuses to delete non-existent fields', done => {
var obj = hasAllPODobject();
obj.save()
.then(() => {
request.put({
url: 'http://localhost:8378/1/schemas/HasAllPOD',
headers: masterKeyHeaders,
json: true,
body: {
fields: {
nonExistentKey: {__op: "Delete"},
}
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(400);
expect(body.code).toEqual(255);
expect(body.error).toEqual('Field nonExistentKey does not exist, cannot delete.');
done();
});
});
});
it('refuses to add a geopoint to a class that already has one', done => {
var obj = hasAllPODobject();
obj.save()
.then(() => {
request.put({
url: 'http://localhost:8378/1/schemas/HasAllPOD',
headers: masterKeyHeaders,
json: true,
body: {
fields: {
newGeo: {type: 'GeoPoint'}
}
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(400);
expect(body.code).toEqual(Parse.Error.INCORRECT_TYPE);
expect(body.error).toEqual('currently, only one GeoPoint field may exist in an object. Adding newGeo when aGeoPoint already exists.');
done();
});
});
});
it('refuses to add two geopoints', done => {
var obj = new Parse.Object('NewClass');
obj.set('aString', 'aString');
obj.save()
.then(() => {
request.put({
url: 'http://localhost:8378/1/schemas/NewClass',
headers: masterKeyHeaders,
json: true,
body: {
fields: {
newGeo1: {type: 'GeoPoint'},
newGeo2: {type: 'GeoPoint'},
}
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(400);
expect(body.code).toEqual(Parse.Error.INCORRECT_TYPE);
expect(body.error).toEqual('currently, only one GeoPoint field may exist in an object. Adding newGeo2 when newGeo1 already exists.');
done();
});
});
});
it('allows you to delete and add a geopoint in the same request', done => {
var obj = new Parse.Object('NewClass');
obj.set('geo1', new Parse.GeoPoint({latitude: 0, longitude: 0}));
obj.save()
.then(() => {
request.put({
url: 'http://localhost:8378/1/schemas/NewClass',
headers: masterKeyHeaders,
json: true,
body: {
fields: {
geo2: {type: 'GeoPoint'},
geo1: {__op: 'Delete'}
}
}
}, (error, response, body) => {
expect(dd(body, {
"className": "NewClass",
"fields": {
"ACL": {"type": "ACL"},
"createdAt": {"type": "Date"},
"objectId": {"type": "String"},
"updatedAt": {"type": "Date"},
"geo2": {"type": "GeoPoint"},
},
classLevelPermissions: defaultClassLevelPermissions
})).toEqual(undefined);
done();
});
})
});
it('put with no modifications returns all fields', done => {
var obj = hasAllPODobject();
obj.save()
.then(() => {
request.put({
url: 'http://localhost:8378/1/schemas/HasAllPOD',
headers: masterKeyHeaders,
json: true,
body: {},
}, (error, response, body) => {
expect(body).toEqual(plainOldDataSchema);
done();
});
})
});
it('lets you add fields', done => {
request.post({
url: 'http://localhost:8378/1/schemas/NewClass',
headers: masterKeyHeaders,
json: true,
body: {},
}, () => {
request.put({
url: 'http://localhost:8378/1/schemas/NewClass',
headers: masterKeyHeaders,
json: true,
body: {
fields: {
newField: {type: 'String'}
}
}
}, (error, response, body) => {
expect(dd(body, {
className: 'NewClass',
fields: {
"ACL": {"type": "ACL"},
"createdAt": {"type": "Date"},
"objectId": {"type": "String"},
"updatedAt": {"type": "Date"},
"newField": {"type": "String"},
},
classLevelPermissions: defaultClassLevelPermissions
})).toEqual(undefined);
request.get({
url: 'http://localhost:8378/1/schemas/NewClass',
headers: masterKeyHeaders,
json: true,
}, (error, response, body) => {
expect(body).toEqual({
className: 'NewClass',
fields: {
ACL: {type: 'ACL'},
createdAt: {type: 'Date'},
updatedAt: {type: 'Date'},
objectId: {type: 'String'},
newField: {type: 'String'},
},
classLevelPermissions: defaultClassLevelPermissions
});
done();
});
});
})
});
it('lets you add fields to system schema', done => {
request.post({
url: 'http://localhost:8378/1/schemas/_User',
headers: masterKeyHeaders,
json: true
}, () => {
request.put({
url: 'http://localhost:8378/1/schemas/_User',
headers: masterKeyHeaders,
json: true,
body: {
fields: {
newField: {type: 'String'}
}
}
}, (error, response, body) => {
expect(dd(body,{
className: '_User',
fields: {
objectId: {type: 'String'},
updatedAt: {type: 'Date'},
createdAt: {type: 'Date'},
username: {type: 'String'},
password: {type: 'String'},
email: {type: 'String'},
emailVerified: {type: 'Boolean'},
authData: {type: 'Object'},
newField: {type: 'String'},
ACL: {type: 'ACL'}
},
classLevelPermissions: defaultClassLevelPermissions
})).toBeUndefined();
request.get({
url: 'http://localhost:8378/1/schemas/_User',
headers: masterKeyHeaders,
json: true
}, (error, response, body) => {
expect(dd(body,{
className: '_User',
fields: {
objectId: {type: 'String'},
updatedAt: {type: 'Date'},
createdAt: {type: 'Date'},
username: {type: 'String'},
password: {type: 'String'},
email: {type: 'String'},
emailVerified: {type: 'Boolean'},
authData: {type: 'Object'},
newField: {type: 'String'},
ACL: {type: 'ACL'}
},
classLevelPermissions: defaultClassLevelPermissions
})).toBeUndefined();
done();
});
});
})
});
it_exclude_dbs(['postgres'])('lets you delete multiple fields and add fields', done => {
var obj1 = hasAllPODobject();
obj1.save()
.then(() => {
request.put({
url: 'http://localhost:8378/1/schemas/HasAllPOD',
headers: masterKeyHeaders,
json: true,
body: {
fields: {
aString: {__op: 'Delete'},
aNumber: {__op: 'Delete'},
aNewString: {type: 'String'},
aNewNumber: {type: 'Number'},
aNewRelation: {type: 'Relation', targetClass: 'HasAllPOD'},
aNewPointer: {type: 'Pointer', targetClass: 'HasAllPOD'},
}
}
}, (error, response, body) => {
expect(body).toEqual({
className: 'HasAllPOD',
fields: {
//Default fields
ACL: {type: 'ACL'},
createdAt: {type: 'Date'},
updatedAt: {type: 'Date'},
objectId: {type: 'String'},
//Custom fields
aBool: {type: 'Boolean'},
aDate: {type: 'Date'},
aObject: {type: 'Object'},
aArray: {type: 'Array'},
aGeoPoint: {type: 'GeoPoint'},
aFile: {type: 'File'},
aNewNumber: {type: 'Number'},
aNewString: {type: 'String'},
aNewPointer: {type: 'Pointer', targetClass: 'HasAllPOD'},
aNewRelation: {type: 'Relation', targetClass: 'HasAllPOD'},
},
classLevelPermissions: defaultClassLevelPermissions
});
var obj2 = new Parse.Object('HasAllPOD');
obj2.set('aNewPointer', obj1);
var relation = obj2.relation('aNewRelation');
relation.add(obj1);
obj2.save().then(done); //Just need to make sure saving works on the new object.
});
});
});
it('will not delete any fields if the additions are invalid', done => {
var obj = hasAllPODobject();
obj.save()
.then(() => {
request.put({
url: 'http://localhost:8378/1/schemas/HasAllPOD',
headers: masterKeyHeaders,
json: true,
body: {
fields: {
fakeNewField: {type: 'fake type'},
aString: {__op: 'Delete'}
}
}
}, (error, response, body) => {
expect(body.code).toEqual(Parse.Error.INCORRECT_TYPE);
expect(body.error).toEqual('invalid field type: fake type');
request.get({
url: 'http://localhost:8378/1/schemas/HasAllPOD',
headers: masterKeyHeaders,
json: true,
}, (error, response) => {
expect(response.body).toEqual(plainOldDataSchema);
done();
});
});
});
});
it('requires the master key to delete schemas', done => {
request.del({
url: 'http://localhost:8378/1/schemas/DoesntMatter',
headers: noAuthHeaders,
json: true,
}, (error, response, body) => {
expect(response.statusCode).toEqual(403);
expect(body.error).toEqual('unauthorized');
done();
});
});
it('refuses to delete non-empty collection', done => {
var obj = hasAllPODobject();
obj.save()
.then(() => {
request.del({
url: 'http://localhost:8378/1/schemas/HasAllPOD',
headers: masterKeyHeaders,
json: true,
}, (error, response, body) => {
expect(response.statusCode).toEqual(400);
expect(body.code).toEqual(255);
expect(body.error).toMatch(/HasAllPOD/);
expect(body.error).toMatch(/contains 1/);
done();
});
});
});
it('fails when deleting collections with invalid class names', done => {
request.del({
url: 'http://localhost:8378/1/schemas/_GlobalConfig',
headers: masterKeyHeaders,
json: true,
}, (error, response, body) => {
expect(response.statusCode).toEqual(400);
expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
expect(body.error).toEqual('Invalid classname: _GlobalConfig, classnames can only have alphanumeric characters and _, and must start with an alpha character ');
done();
})
});
it('does not fail when deleting nonexistant collections', done => {
request.del({
url: 'http://localhost:8378/1/schemas/Missing',
headers: masterKeyHeaders,
json: true,
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(body).toEqual({});
done();
});
});
it('deletes collections including join tables', done => {
var obj = new Parse.Object('MyClass');
obj.set('data', 'data');
obj.save()
.then(() => {
var obj2 = new Parse.Object('MyOtherClass');
var relation = obj2.relation('aRelation');
relation.add(obj);
return obj2.save();
})
.then(obj2 => obj2.destroy())
.then(() => {
request.del({
url: 'http://localhost:8378/1/schemas/MyOtherClass',
headers: masterKeyHeaders,
json: true,
}, (error, response) => {
expect(response.statusCode).toEqual(200);
expect(response.body).toEqual({});
config.database.collectionExists('_Join:aRelation:MyOtherClass').then(exists => {
if (exists) {
fail('Relation collection should be deleted.');
done();
}
return config.database.collectionExists('MyOtherClass');
}).then(exists => {
if (exists) {
fail('Class collection should be deleted.');
done();
}
}).then(() => {
request.get({
url: 'http://localhost:8378/1/schemas/MyOtherClass',
headers: masterKeyHeaders,
json: true,
}, (error, response, body) => {
//Expect _SCHEMA entry to be gone.
expect(response.statusCode).toEqual(400);
expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
expect(body.error).toEqual('Class MyOtherClass does not exist.');
done();
});
});
});
}).then(() => {
}, error => {
fail(error);
done();
});
});
it('deletes schema when actual collection does not exist', done => {
request.post({
url: 'http://localhost:8378/1/schemas/NewClassForDelete',
headers: masterKeyHeaders,
json: true,
body: {
className: 'NewClassForDelete'
}
}, (error, response) => {
expect(error).toEqual(null);
expect(response.body.className).toEqual('NewClassForDelete');
request.del({
url: 'http://localhost:8378/1/schemas/NewClassForDelete',
headers: masterKeyHeaders,
json: true,
}, (error, response) => {
expect(response.statusCode).toEqual(200);
expect(response.body).toEqual({});
config.database.loadSchema().then(schema => {
schema.hasClass('NewClassForDelete').then(exist => {
expect(exist).toEqual(false);
done();
});
})
});
});
});
it('deletes schema when actual collection exists', done => {
request.post({
url: 'http://localhost:8378/1/schemas/NewClassForDelete',
headers: masterKeyHeaders,
json: true,
body: {
className: 'NewClassForDelete'
}
}, (error, response) => {
expect(error).toEqual(null);
expect(response.body.className).toEqual('NewClassForDelete');
request.post({
url: 'http://localhost:8378/1/classes/NewClassForDelete',
headers: restKeyHeaders,
json: true
}, (error, response) => {
expect(error).toEqual(null);
expect(typeof response.body.objectId).toEqual('string');
request.del({
url: 'http://localhost:8378/1/classes/NewClassForDelete/' + response.body.objectId,
headers: restKeyHeaders,
json: true,
}, (error) => {
expect(error).toEqual(null);
request.del({
url: 'http://localhost:8378/1/schemas/NewClassForDelete',
headers: masterKeyHeaders,
json: true,
}, (error, response) => {
expect(response.statusCode).toEqual(200);
expect(response.body).toEqual({});
config.database.loadSchema().then(schema => {
schema.hasClass('NewClassForDelete').then(exist => {
expect(exist).toEqual(false);
done();
});
});
});
});
});
});
});
it('should set/get schema permissions', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
'*': true
},
create: {
'role:admin': true
}
}
}
}, (error) => {
expect(error).toEqual(null);
request.get({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
}, (error, response) => {
expect(response.statusCode).toEqual(200);
expect(response.body.classLevelPermissions).toEqual({
find: {
'*': true
},
create: {
'role:admin': true
},
get: {},
update: {},
delete: {},
addField: {}
});
done();
});
});
});
it('should fail setting schema permissions with invalid key', done => {
const object = new Parse.Object('AClass');
object.save().then(() => {
request.put({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
'*': true
},
create: {
'role:admin': true
},
dummy: {
'some': true
}
}
}
}, (error, response, body) => {
expect(error).toEqual(null);
expect(body.code).toEqual(107);
expect(body.error).toEqual('dummy is not a valid operation for class level permissions');
done();
});
});
});
it('should not be able to add a field', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
create: {
'*': true
},
find: {
'*': true
},
addField: {
'role:admin': true
}
}
}
}, (error) => {
expect(error).toEqual(null);
const object = new Parse.Object('AClass');
object.set('hello', 'world');
return object.save().then(() => {
fail('should not be able to add a field');
done();
}, (err) => {
expect(err.message).toEqual('Permission denied for action addField on class AClass.');
done();
})
})
});
it('should be able to add a field', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
create: {
'*': true
},
addField: {
'*': true
}
}
}
}, (error) => {
expect(error).toEqual(null);
const object = new Parse.Object('AClass');
object.set('hello', 'world');
return object.save().then(() => {
done();
}, () => {
fail('should be able to add a field');
done();
})
})
});
it('should throw with invalid userId (>10 chars)', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
'1234567890A': true
},
}
}
}, (error, response, body) => {
expect(body.error).toEqual("'1234567890A' is not a valid key for class level permissions");
done();
})
});
it('should throw with invalid userId (<10 chars)', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
'a12345678': true
},
}
}
}, (error, response, body) => {
expect(body.error).toEqual("'a12345678' is not a valid key for class level permissions");
done();
})
});
it('should throw with invalid userId (invalid char)', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
'12345_6789': true
},
}
}
}, (error, response, body) => {
expect(body.error).toEqual("'12345_6789' is not a valid key for class level permissions");
done();
})
});
it('should throw with invalid * (spaces)', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
' *': true
},
}
}
}, (error, response, body) => {
expect(body.error).toEqual("' *' is not a valid key for class level permissions");
done();
})
});
it('should throw with invalid * (spaces)', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
'* ': true
},
}
}
}, (error, response, body) => {
expect(body.error).toEqual("'* ' is not a valid key for class level permissions");
done();
})
});
it('should throw with invalid value', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
'*': 1
},
}
}
}, (error, response, body) => {
expect(body.error).toEqual("'1' is not a valid value for class level permissions find:*:1");
done();
})
});
it('should throw with invalid value', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
'*': ""
},
}
}
}, (error, response, body) => {
expect(body.error).toEqual("'' is not a valid value for class level permissions find:*:");
done();
})
});
function setPermissionsOnClass(className, permissions, doPut) {
let op = request.post;
if (doPut)
{
op = request.put;
}
return new Promise((resolve, reject) => {
op({
url: 'http://localhost:8378/1/schemas/'+className,
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: permissions
}
}, (error, response, body) => {
if (error) {
return reject(error);
}
if (body.error) {
return reject(body);
}
return resolve(body);
})
});
}
it('validate CLP 1', done => {
const user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
const admin = new Parse.User();
admin.setUsername('admin');
admin.setPassword('admin');
const role = new Parse.Role('admin', new Parse.ACL());
setPermissionsOnClass('AClass', {
'find': {
'role:admin': true
}
}).then(() => {
return Parse.Object.saveAll([user, admin, role], {useMasterKey: true});
}).then(()=> {
role.relation('users').add(admin);
return role.save(null, {useMasterKey: true});
}).then(() => {
return Parse.User.logIn('user', 'user').then(() => {
const obj = new Parse.Object('AClass');
return obj.save(null, {useMasterKey: true});
})
}).then(() => {
const query = new Parse.Query('AClass');
return query.find().then(() => {
fail('Use should hot be able to find!')
}, (err) => {
expect(err.message).toEqual('Permission denied for action find on class AClass.');
return Promise.resolve();
})
}).then(() => {
return Parse.User.logIn('admin', 'admin');
}).then(() => {
const query = new Parse.Query('AClass');
return query.find();
}).then((results) => {
expect(results.length).toBe(1);
done();
}).catch((err) => {
jfail(err);
done();
})
});
it('validate CLP 2', done => {
const user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
const admin = new Parse.User();
admin.setUsername('admin');
admin.setPassword('admin');
const role = new Parse.Role('admin', new Parse.ACL());
setPermissionsOnClass('AClass', {
'find': {
'role:admin': true
}
}).then(() => {
return Parse.Object.saveAll([user, admin, role], {useMasterKey: true});
}).then(()=> {
role.relation('users').add(admin);
return role.save(null, {useMasterKey: true});
}).then(() => {
return Parse.User.logIn('user', 'user').then(() => {
const obj = new Parse.Object('AClass');
return obj.save(null, {useMasterKey: true});
})
}).then(() => {
const query = new Parse.Query('AClass');
return query.find().then(() => {
fail('User should not be able to find!')
}, (err) => {
expect(err.message).toEqual('Permission denied for action find on class AClass.');
return Promise.resolve();
})
}).then(() => {
// let everyone see it now
return setPermissionsOnClass('AClass', {
'find': {
'role:admin': true,
'*': true
}
}, true);
}).then(() => {
const query = new Parse.Query('AClass');
return query.find().then((result) => {
expect(result.length).toBe(1);
}, () => {
fail('User should be able to find!')
done();
});
}).then(() => {
return Parse.User.logIn('admin', 'admin');
}).then(() => {
const query = new Parse.Query('AClass');
return query.find();
}).then((results) => {
expect(results.length).toBe(1);
done();
}).catch((err) => {
jfail(err);
done();
})
});
it('validate CLP 3', done => {
const user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
const admin = new Parse.User();
admin.setUsername('admin');
admin.setPassword('admin');
const role = new Parse.Role('admin', new Parse.ACL());
setPermissionsOnClass('AClass', {
'find': {
'role:admin': true
}
}).then(() => {
return Parse.Object.saveAll([user, admin, role], {useMasterKey: true});
}).then(()=> {
role.relation('users').add(admin);
return role.save(null, {useMasterKey: true});
}).then(() => {
return Parse.User.logIn('user', 'user').then(() => {
const obj = new Parse.Object('AClass');
return obj.save(null, {useMasterKey: true});
})
}).then(() => {
const query = new Parse.Query('AClass');
return query.find().then(() => {
fail('User should not be able to find!')
}, (err) => {
expect(err.message).toEqual('Permission denied for action find on class AClass.');
return Promise.resolve();
})
}).then(() => {
// delete all CLP
return setPermissionsOnClass('AClass', null, true);
}).then(() => {
const query = new Parse.Query('AClass');
return query.find().then((result) => {
expect(result.length).toBe(1);
}, () => {
fail('User should be able to find!')
done();
});
}).then(() => {
return Parse.User.logIn('admin', 'admin');
}).then(() => {
const query = new Parse.Query('AClass');
return query.find();
}).then((results) => {
expect(results.length).toBe(1);
done();
}).catch((err) => {
jfail(err);
done();
});
});
it('validate CLP 4', done => {
const user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
const admin = new Parse.User();
admin.setUsername('admin');
admin.setPassword('admin');
const role = new Parse.Role('admin', new Parse.ACL());
setPermissionsOnClass('AClass', {
'find': {
'role:admin': true
}
}).then(() => {
return Parse.Object.saveAll([user, admin, role], {useMasterKey: true});
}).then(()=> {
role.relation('users').add(admin);
return role.save(null, {useMasterKey: true});
}).then(() => {
return Parse.User.logIn('user', 'user').then(() => {
const obj = new Parse.Object('AClass');
return obj.save(null, {useMasterKey: true});
})
}).then(() => {
const query = new Parse.Query('AClass');
return query.find().then(() => {
fail('User should not be able to find!')
}, (err) => {
expect(err.message).toEqual('Permission denied for action find on class AClass.');
return Promise.resolve();
})
}).then(() => {
// borked CLP should not affec security
return setPermissionsOnClass('AClass', {
'found': {
'role:admin': true
}
}, true).then(() => {
fail("Should not be able to save a borked CLP");
}, () => {
return Promise.resolve();
})
}).then(() => {
const query = new Parse.Query('AClass');
return query.find().then(() => {
fail('User should not be able to find!')
}, (err) => {
expect(err.message).toEqual('Permission denied for action find on class AClass.');
return Promise.resolve();
});
}).then(() => {
return Parse.User.logIn('admin', 'admin');
}).then(() => {
const query = new Parse.Query('AClass');
return query.find();
}).then((results) => {
expect(results.length).toBe(1);
done();
}).catch((err) => {
jfail(err);
done();
})
});
it('validate CLP 5', done => {
const user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
const user2 = new Parse.User();
user2.setUsername('user2');
user2.setPassword('user2');
const admin = new Parse.User();
admin.setUsername('admin');
admin.setPassword('admin');
const role = new Parse.Role('admin', new Parse.ACL());
Promise.resolve().then(() => {
return Parse.Object.saveAll([user, user2, admin, role], {useMasterKey: true});
}).then(()=> {
role.relation('users').add(admin);
return role.save(null, {useMasterKey: true}).then(() => {
const perm = {
find: {}
};
// let the user find
perm['find'][user.id] = true;
return setPermissionsOnClass('AClass', perm);
})
}).then(() => {
return Parse.User.logIn('user', 'user').then(() => {
const obj = new Parse.Object('AClass');
return obj.save();
})
}).then(() => {
const query = new Parse.Query('AClass');
return query.find().then((res) => {
expect(res.length).toEqual(1);
}, () => {
fail('User should be able to find!')
return Promise.resolve();
})
}).then(() => {
return Parse.User.logIn('admin', 'admin');
}).then(() => {
const query = new Parse.Query('AClass');
return query.find();
}).then(() => {
fail("should not be able to read!");
return Promise.resolve();
}, (err) => {
expect(err.message).toEqual('Permission denied for action create on class AClass.');
return Promise.resolve();
}).then(() => {
return Parse.User.logIn('user2', 'user2');
}).then(() => {
const query = new Parse.Query('AClass');
return query.find();
}).then(() => {
fail("should not be able to read!");
return Promise.resolve();
}, (err) => {
expect(err.message).toEqual('Permission denied for action find on class AClass.');
return Promise.resolve();
}).then(() => {
done();
});
});
it('can query with include and CLP (issue #2005)', (done) => {
setPermissionsOnClass('AnotherObject', {
get: {"*": true},
find: {},
create: {'*': true},
update: {'*': true},
delete: {'*': true},
addField:{'*': true}
}).then(() => {
const obj = new Parse.Object('AnObject');
const anotherObject = new Parse.Object('AnotherObject');
return obj.save({
anotherObject
})
}).then(() => {
const query = new Parse.Query('AnObject');
query.include('anotherObject');
return query.find();
}).then((res) => {
expect(res.length).toBe(1);
expect(res[0].get('anotherObject')).not.toBeUndefined();
done();
}).catch((err) => {
jfail(err);
done();
})
});
it('can add field as master (issue #1257)', (done) => {
setPermissionsOnClass('AClass', {
'addField': {}
}).then(() => {
var obj = new Parse.Object('AClass');
obj.set('key', 'value');
return obj.save(null, {useMasterKey: true})
}).then((obj) => {
expect(obj.get('key')).toEqual('value');
done();
}, () => {
fail('should not fail');
done();
});
});
it('can login when addFields is false (issue #1355)', (done) => {
setPermissionsOnClass('_User', {
'create': {'*': true},
'addField': {}
}, true).then(() => {
return Parse.User.signUp('foo', 'bar');
}).then((user) => {
expect(user.getUsername()).toBe('foo');
done()
}, error => {
fail(JSON.stringify(error));
done();
})
})
it('gives correct response when deleting a schema with CLPs (regression test #1919)', done => {
new Parse.Object('MyClass').save({ data: 'foo'})
.then(obj => obj.destroy())
.then(() => setPermissionsOnClass('MyClass', { find: {}, get: {} }, true))
.then(() => {
request.del({
url: 'http://localhost:8378/1/schemas/MyClass',
headers: masterKeyHeaders,
json: true,
}, (error, response) => {
expect(response.statusCode).toEqual(200);
expect(response.body).toEqual({});
done();
});
});
});
it("regression test for #1991", done => {
const user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
const role = new Parse.Role('admin', new Parse.ACL());
const obj = new Parse.Object('AnObject');
Parse.Object.saveAll([user, role]).then(() => {
role.relation('users').add(user);
return role.save(null, {useMasterKey: true});
}).then(() => {
return setPermissionsOnClass('AnObject', {
'get': {"*": true},
'find': {"*": true},
'create': {'*': true},
'update': {'role:admin': true},
'delete': {'role:admin': true}
})
}).then(() => {
return obj.save();
}).then(() => {
return Parse.User.logIn('user', 'user')
}).then(() => {
return obj.destroy();
}).then(() => {
const query = new Parse.Query('AnObject');
return query.find();
}).then((results) => {
expect(results.length).toBe(0);
done();
}).catch((err) => {
fail('should not fail');
jfail(err);
done();
});
});
it('regression test for #2246', done => {
const profile = new Parse.Object('UserProfile');
const user = new Parse.User();
function initialize() {
return user.save({
username: 'user',
password: 'password'
}).then(() => {
return profile.save({user}).then(() => {
return user.save({
userProfile: profile
}, {useMasterKey: true});
});
});
}
initialize().then(() => {
return setPermissionsOnClass('UserProfile', {
'readUserFields': ['user'],
'writeUserFields': ['user']
}, true);
}).then(() => {
return Parse.User.logIn('user', 'password')
}).then(() => {
const query = new Parse.Query('_User');
query.include('userProfile');
return query.get(user.id);
}).then((user) => {
expect(user.get('userProfile')).not.toBeUndefined();
done();
}, (err) => {
jfail(err);
done();
});
});
});