3743 lines
106 KiB
JavaScript
3743 lines
106 KiB
JavaScript
'use strict';
|
|
|
|
const Parse = require('parse/node').Parse;
|
|
const dd = require('deep-diff');
|
|
const Config = require('../lib/Config');
|
|
const request = require('../lib/request');
|
|
const TestUtils = require('../lib/TestUtils');
|
|
const SchemaController = require('../lib/Controllers/SchemaController').SchemaController;
|
|
|
|
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=' }));
|
|
const objACL = new Parse.ACL();
|
|
objACL.setPublicWriteAccess(false);
|
|
obj.setACL(objACL);
|
|
return obj;
|
|
};
|
|
|
|
const defaultClassLevelPermissions = {
|
|
find: {
|
|
'*': true,
|
|
},
|
|
count: {
|
|
'*': true,
|
|
},
|
|
create: {
|
|
'*': true,
|
|
},
|
|
get: {
|
|
'*': true,
|
|
},
|
|
update: {
|
|
'*': true,
|
|
},
|
|
addField: {
|
|
'*': true,
|
|
},
|
|
delete: {
|
|
'*': true,
|
|
},
|
|
protectedFields: {
|
|
'*': [],
|
|
},
|
|
};
|
|
|
|
const 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,
|
|
};
|
|
|
|
const 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,
|
|
};
|
|
|
|
const roleSchema = {
|
|
className: '_Role',
|
|
fields: {
|
|
objectId: { type: 'String' },
|
|
createdAt: { type: 'Date' },
|
|
updatedAt: { type: 'Date' },
|
|
ACL: { type: 'ACL' },
|
|
name: { type: 'String' },
|
|
users: { type: 'Relation', targetClass: '_User' },
|
|
roles: { type: 'Relation', targetClass: '_Role' },
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
};
|
|
|
|
const noAuthHeaders = {
|
|
'X-Parse-Application-Id': 'test',
|
|
};
|
|
|
|
const restKeyHeaders = {
|
|
'X-Parse-Application-Id': 'test',
|
|
'X-Parse-REST-API-Key': 'rest',
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
const masterKeyHeaders = {
|
|
'X-Parse-Application-Id': 'test',
|
|
'X-Parse-Master-Key': 'test',
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
describe('schemas', () => {
|
|
beforeEach(async () => {
|
|
await reconfigureServer();
|
|
config = Config.get('test');
|
|
});
|
|
|
|
it('requires the master key to get all schemas', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
json: true,
|
|
headers: noAuthHeaders,
|
|
}).then(fail, response => {
|
|
//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.status).toEqual(403);
|
|
expect(response.data.error).toEqual('unauthorized');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('requires the master key to get one schema', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/SomeSchema',
|
|
json: true,
|
|
headers: restKeyHeaders,
|
|
}).then(fail, response => {
|
|
expect(response.status).toEqual(403);
|
|
expect(response.data.error).toEqual('unauthorized: master key is required');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('asks for the master key if you use the rest key', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
json: true,
|
|
headers: restKeyHeaders,
|
|
}).then(fail, response => {
|
|
expect(response.status).toEqual(403);
|
|
expect(response.data.error).toEqual('unauthorized: master key is required');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('creates _User schema when server starts', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
json: true,
|
|
headers: masterKeyHeaders,
|
|
}).then(response => {
|
|
const expected = {
|
|
results: [userSchema, roleSchema],
|
|
};
|
|
expect(
|
|
response.data.results
|
|
.sort((s1, s2) => s1.className.localeCompare(s2.className))
|
|
.map(s => {
|
|
const withoutIndexes = Object.assign({}, s);
|
|
delete withoutIndexes.indexes;
|
|
return withoutIndexes;
|
|
})
|
|
).toEqual(expected.results.sort((s1, s2) => s1.className.localeCompare(s2.className)));
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('responds with a list of schemas after creating objects', 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(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
json: true,
|
|
headers: masterKeyHeaders,
|
|
}).then(response => {
|
|
const expected = {
|
|
results: [userSchema, roleSchema, plainOldDataSchema, pointersAndRelationsSchema],
|
|
};
|
|
expect(
|
|
response.data.results
|
|
.sort((s1, s2) => s1.className.localeCompare(s2.className))
|
|
.map(s => {
|
|
const withoutIndexes = Object.assign({}, s);
|
|
delete withoutIndexes.indexes;
|
|
return withoutIndexes;
|
|
})
|
|
).toEqual(expected.results.sort((s1, s2) => s1.className.localeCompare(s2.className)));
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('ensure refresh cache after creating a class', async done => {
|
|
spyOn(SchemaController.prototype, 'reloadData').and.callFake(() => Promise.resolve());
|
|
await request({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'A',
|
|
},
|
|
});
|
|
const response = await request({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
method: 'GET',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
});
|
|
const expected = {
|
|
results: [
|
|
userSchema,
|
|
roleSchema,
|
|
{
|
|
className: 'A',
|
|
fields: {
|
|
//Default fields
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
updatedAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
},
|
|
],
|
|
};
|
|
expect(
|
|
response.data.results
|
|
.sort((s1, s2) => s1.className.localeCompare(s2.className))
|
|
.map(s => {
|
|
const withoutIndexes = Object.assign({}, s);
|
|
delete withoutIndexes.indexes;
|
|
return withoutIndexes;
|
|
})
|
|
).toEqual(expected.results.sort((s1, s2) => s1.className.localeCompare(s2.className)));
|
|
done();
|
|
});
|
|
|
|
it('responds with a single schema', done => {
|
|
const obj = hasAllPODobject();
|
|
obj.save().then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/HasAllPOD',
|
|
json: true,
|
|
headers: masterKeyHeaders,
|
|
}).then(response => {
|
|
expect(response.data).toEqual(plainOldDataSchema);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('treats class names case sensitively', done => {
|
|
const obj = hasAllPODobject();
|
|
obj.save().then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/HASALLPOD',
|
|
json: true,
|
|
headers: masterKeyHeaders,
|
|
}).then(fail, response => {
|
|
expect(response.status).toEqual(400);
|
|
expect(response.data).toEqual({
|
|
code: 103,
|
|
error: 'Class HASALLPOD does not exist.',
|
|
});
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('requires the master key to create a schema', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
method: 'POST',
|
|
json: true,
|
|
headers: noAuthHeaders,
|
|
body: {
|
|
className: 'MyClass',
|
|
},
|
|
}).then(fail, response => {
|
|
expect(response.status).toEqual(403);
|
|
expect(response.data.error).toEqual('unauthorized');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('sends an error if you use mismatching class names', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/A',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'B',
|
|
},
|
|
}).then(fail, response => {
|
|
expect(response.status).toEqual(400);
|
|
expect(response.data).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({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {},
|
|
}).then(fail, response => {
|
|
expect(response.status).toEqual(400);
|
|
expect(response.data).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({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'A',
|
|
},
|
|
}).then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'A',
|
|
},
|
|
}).then(fail, response => {
|
|
expect(response.status).toEqual(400);
|
|
expect(response.data).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({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'NewClass',
|
|
fields: {
|
|
foo: { type: 'Number' },
|
|
ptr: { type: 'Pointer', targetClass: 'SomeClass' },
|
|
},
|
|
},
|
|
}).then(response => {
|
|
expect(response.data).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 and options when you create a class with field options', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'NewClassWithOptions',
|
|
fields: {
|
|
foo1: { type: 'Number' },
|
|
foo2: { type: 'Number', required: true, defaultValue: 10 },
|
|
foo3: {
|
|
type: 'String',
|
|
required: false,
|
|
defaultValue: 'some string',
|
|
},
|
|
foo4: { type: 'Date', required: true },
|
|
foo5: { type: 'Number', defaultValue: 5 },
|
|
ptr: { type: 'Pointer', targetClass: 'SomeClass', required: false },
|
|
defaultFalse: {
|
|
type: 'Boolean',
|
|
required: true,
|
|
defaultValue: false,
|
|
},
|
|
defaultZero: { type: 'Number', defaultValue: 0 },
|
|
relation: { type: 'Relation', targetClass: 'SomeClass' },
|
|
},
|
|
},
|
|
}).then(async response => {
|
|
expect(response.data).toEqual({
|
|
className: 'NewClassWithOptions',
|
|
fields: {
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
updatedAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
foo1: { type: 'Number' },
|
|
foo2: { type: 'Number', required: true, defaultValue: 10 },
|
|
foo3: {
|
|
type: 'String',
|
|
required: false,
|
|
defaultValue: 'some string',
|
|
},
|
|
foo4: { type: 'Date', required: true },
|
|
foo5: { type: 'Number', defaultValue: 5 },
|
|
ptr: { type: 'Pointer', targetClass: 'SomeClass', required: false },
|
|
defaultFalse: {
|
|
type: 'Boolean',
|
|
required: true,
|
|
defaultValue: false,
|
|
},
|
|
defaultZero: { type: 'Number', defaultValue: 0 },
|
|
relation: { type: 'Relation', targetClass: 'SomeClass' },
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
});
|
|
const obj = new Parse.Object('NewClassWithOptions');
|
|
try {
|
|
await obj.save();
|
|
fail('should fail');
|
|
} catch (e) {
|
|
expect(e.code).toEqual(142);
|
|
}
|
|
const date = new Date();
|
|
obj.set('foo4', date);
|
|
await obj.save();
|
|
expect(obj.get('foo1')).toBeUndefined();
|
|
expect(obj.get('foo2')).toEqual(10);
|
|
expect(obj.get('foo3')).toEqual('some string');
|
|
expect(obj.get('foo4')).toEqual(date);
|
|
expect(obj.get('foo5')).toEqual(5);
|
|
expect(obj.get('ptr')).toBeUndefined();
|
|
expect(obj.get('defaultFalse')).toEqual(false);
|
|
expect(obj.get('defaultZero')).toEqual(0);
|
|
expect(obj.get('ptr')).toBeUndefined();
|
|
expect(obj.get('relation')).toBeUndefined();
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('try to set a relation field as a required field', async done => {
|
|
try {
|
|
await request({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'NewClassWithRelationRequired',
|
|
fields: {
|
|
foo: { type: 'String' },
|
|
relation: {
|
|
type: 'Relation',
|
|
targetClass: 'SomeClass',
|
|
required: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
fail('should fail');
|
|
} catch (e) {
|
|
expect(e.data.code).toEqual(111);
|
|
}
|
|
done();
|
|
});
|
|
|
|
it('try to set a relation field with a default value', async done => {
|
|
try {
|
|
await request({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'NewClassRelationWithOptions',
|
|
fields: {
|
|
foo: { type: 'String' },
|
|
relation: {
|
|
type: 'Relation',
|
|
targetClass: 'SomeClass',
|
|
defaultValue: { __type: 'Relation', className: '_User' },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
fail('should fail');
|
|
} catch (e) {
|
|
expect(e.data.code).toEqual(111);
|
|
}
|
|
done();
|
|
});
|
|
|
|
it('try to update schemas with a relation field with options', async done => {
|
|
await request({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'NewClassRelationWithOptions',
|
|
fields: {
|
|
foo: { type: 'String' },
|
|
},
|
|
},
|
|
});
|
|
try {
|
|
await request({
|
|
url: 'http://localhost:8378/1/schemas/NewClassRelationWithOptions',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'NewClassRelationWithOptions',
|
|
fields: {
|
|
relation: {
|
|
type: 'Relation',
|
|
targetClass: 'SomeClass',
|
|
required: true,
|
|
},
|
|
},
|
|
_method: 'PUT',
|
|
},
|
|
});
|
|
fail('should fail');
|
|
} catch (e) {
|
|
expect(e.data.code).toEqual(111);
|
|
}
|
|
|
|
try {
|
|
await request({
|
|
url: 'http://localhost:8378/1/schemas/NewClassRelationWithOptions',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'NewClassRelationWithOptions',
|
|
fields: {
|
|
relation: {
|
|
type: 'Relation',
|
|
targetClass: 'SomeClass',
|
|
defaultValue: { __type: 'Relation', className: '_User' },
|
|
},
|
|
},
|
|
_method: 'PUT',
|
|
},
|
|
});
|
|
fail('should fail');
|
|
} catch (e) {
|
|
expect(e.data.code).toEqual(111);
|
|
}
|
|
done();
|
|
});
|
|
|
|
it('validated the data type of default values when creating a new class', async () => {
|
|
try {
|
|
await request({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'NewClassWithValidation',
|
|
fields: {
|
|
foo: { type: 'String', defaultValue: 10 },
|
|
},
|
|
},
|
|
});
|
|
fail('should fail');
|
|
} catch (e) {
|
|
expect(e.data.error).toEqual(
|
|
'schema mismatch for NewClassWithValidation.foo default value; expected String but got Number'
|
|
);
|
|
}
|
|
});
|
|
|
|
it('validated the data type of default values when adding new fields', async () => {
|
|
try {
|
|
await request({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'NewClassWithValidation',
|
|
fields: {
|
|
foo: { type: 'String', defaultValue: 'some value' },
|
|
},
|
|
},
|
|
});
|
|
await request({
|
|
url: 'http://localhost:8378/1/schemas/NewClassWithValidation',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'NewClassWithValidation',
|
|
fields: {
|
|
foo2: { type: 'String', defaultValue: 10 },
|
|
},
|
|
},
|
|
});
|
|
fail('should fail');
|
|
} catch (e) {
|
|
expect(e.data.error).toEqual(
|
|
'schema mismatch for NewClassWithValidation.foo2 default value; expected String but got Number'
|
|
);
|
|
}
|
|
});
|
|
|
|
it('responds with all fields when getting incomplete schema', done => {
|
|
config.database
|
|
.loadSchema()
|
|
.then(schemaController =>
|
|
schemaController.addClassIfNotExists('_Installation', {}, defaultClassLevelPermissions)
|
|
)
|
|
.then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/_Installation',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
}).then(response => {
|
|
expect(
|
|
dd(response.data, {
|
|
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({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'NewClass',
|
|
},
|
|
}).then(response => {
|
|
expect(response.data).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({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {},
|
|
}).then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'PUT',
|
|
headers: noAuthHeaders,
|
|
json: true,
|
|
body: {},
|
|
}).then(fail, response => {
|
|
expect(response.status).toEqual(403);
|
|
expect(response.data.error).toEqual('unauthorized');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('rejects class name mis-matches in put', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: { className: 'WrongClassName' },
|
|
}).then(fail, response => {
|
|
expect(response.status).toEqual(400);
|
|
expect(response.data.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
|
expect(response.data.error).toEqual(
|
|
'Class name mismatch between WrongClassName and NewClass.'
|
|
);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('refuses to add fields to non-existent classes', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NoClass',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
fields: {
|
|
newField: { type: 'String' },
|
|
},
|
|
},
|
|
}).then(fail, response => {
|
|
expect(response.status).toEqual(400);
|
|
expect(response.data.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
|
expect(response.data.error).toEqual('Class NoClass does not exist.');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('refuses to put to existing fields with different type, even if it would not be a change', done => {
|
|
const obj = hasAllPODobject();
|
|
obj.save().then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/HasAllPOD',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
fields: {
|
|
aString: { type: 'Number' },
|
|
},
|
|
},
|
|
}).then(fail, response => {
|
|
expect(response.status).toEqual(400);
|
|
expect(response.data.code).toEqual(255);
|
|
expect(response.data.error).toEqual('Field aString exists, cannot update.');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('refuses to delete non-existent fields', done => {
|
|
const obj = hasAllPODobject();
|
|
obj.save().then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/HasAllPOD',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
fields: {
|
|
nonExistentKey: { __op: 'Delete' },
|
|
},
|
|
},
|
|
}).then(fail, response => {
|
|
expect(response.status).toEqual(400);
|
|
expect(response.data.code).toEqual(255);
|
|
expect(response.data.error).toEqual('Field nonExistentKey does not exist, cannot delete.');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('refuses to add a geopoint to a class that already has one', done => {
|
|
const obj = hasAllPODobject();
|
|
obj.save().then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/HasAllPOD',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
fields: {
|
|
newGeo: { type: 'GeoPoint' },
|
|
},
|
|
},
|
|
}).then(fail, response => {
|
|
expect(response.status).toEqual(400);
|
|
expect(response.data.code).toEqual(Parse.Error.INCORRECT_TYPE);
|
|
expect(response.data.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 => {
|
|
const obj = new Parse.Object('NewClass');
|
|
obj.set('aString', 'aString');
|
|
obj.save().then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
fields: {
|
|
newGeo1: { type: 'GeoPoint' },
|
|
newGeo2: { type: 'GeoPoint' },
|
|
},
|
|
},
|
|
}).then(fail, response => {
|
|
expect(response.status).toEqual(400);
|
|
expect(response.data.code).toEqual(Parse.Error.INCORRECT_TYPE);
|
|
expect(response.data.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 => {
|
|
const obj = new Parse.Object('NewClass');
|
|
obj.set('geo1', new Parse.GeoPoint({ latitude: 0, longitude: 0 }));
|
|
obj.save().then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
fields: {
|
|
geo2: { type: 'GeoPoint' },
|
|
geo1: { __op: 'Delete' },
|
|
},
|
|
},
|
|
}).then(response => {
|
|
expect(
|
|
dd(response.data, {
|
|
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 => {
|
|
const obj = hasAllPODobject();
|
|
obj.save().then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/HasAllPOD',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {},
|
|
}).then(response => {
|
|
expect(response.data).toEqual(plainOldDataSchema);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('lets you add fields', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {},
|
|
}).then(() => {
|
|
request({
|
|
method: 'PUT',
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
fields: {
|
|
newField: { type: 'String' },
|
|
},
|
|
},
|
|
}).then(response => {
|
|
expect(
|
|
dd(response.data, {
|
|
className: 'NewClass',
|
|
fields: {
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
updatedAt: { type: 'Date' },
|
|
newField: { type: 'String' },
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
})
|
|
).toEqual(undefined);
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
}).then(response => {
|
|
expect(response.data).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 with options', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {},
|
|
}).then(() => {
|
|
request({
|
|
method: 'PUT',
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
fields: {
|
|
newField: {
|
|
type: 'String',
|
|
required: true,
|
|
defaultValue: 'some value',
|
|
},
|
|
},
|
|
},
|
|
}).then(response => {
|
|
expect(
|
|
dd(response.data, {
|
|
className: 'NewClass',
|
|
fields: {
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
updatedAt: { type: 'Date' },
|
|
newField: {
|
|
type: 'String',
|
|
required: true,
|
|
defaultValue: 'some value',
|
|
},
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
})
|
|
).toEqual(undefined);
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
}).then(response => {
|
|
expect(response.data).toEqual({
|
|
className: 'NewClass',
|
|
fields: {
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
updatedAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
newField: {
|
|
type: 'String',
|
|
required: true,
|
|
defaultValue: 'some value',
|
|
},
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
});
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should validate required fields', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {},
|
|
}).then(() => {
|
|
request({
|
|
method: 'PUT',
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
fields: {
|
|
newRequiredField: {
|
|
type: 'String',
|
|
required: true,
|
|
},
|
|
newRequiredFieldWithDefaultValue: {
|
|
type: 'String',
|
|
required: true,
|
|
defaultValue: 'some value',
|
|
},
|
|
newNotRequiredField: {
|
|
type: 'String',
|
|
required: false,
|
|
},
|
|
newNotRequiredFieldWithDefaultValue: {
|
|
type: 'String',
|
|
required: false,
|
|
defaultValue: 'some value',
|
|
},
|
|
newRegularFieldWithDefaultValue: {
|
|
type: 'String',
|
|
defaultValue: 'some value',
|
|
},
|
|
newRegularField: {
|
|
type: 'String',
|
|
},
|
|
},
|
|
},
|
|
}).then(async () => {
|
|
let obj = new Parse.Object('NewClass');
|
|
try {
|
|
await obj.save();
|
|
fail('Should fail');
|
|
} catch (e) {
|
|
expect(e.code).toEqual(142);
|
|
expect(e.message).toEqual('newRequiredField is required');
|
|
}
|
|
obj.set('newRequiredField', 'some value');
|
|
await obj.save();
|
|
expect(obj.get('newRequiredField')).toEqual('some value');
|
|
expect(obj.get('newRequiredFieldWithDefaultValue')).toEqual('some value');
|
|
expect(obj.get('newNotRequiredField')).toEqual(undefined);
|
|
expect(obj.get('newNotRequiredFieldWithDefaultValue')).toEqual('some value');
|
|
expect(obj.get('newRegularField')).toEqual(undefined);
|
|
obj.set('newRequiredField', null);
|
|
try {
|
|
await obj.save();
|
|
fail('Should fail');
|
|
} catch (e) {
|
|
expect(e.code).toEqual(142);
|
|
expect(e.message).toEqual('newRequiredField is required');
|
|
}
|
|
obj.unset('newRequiredField');
|
|
try {
|
|
await obj.save();
|
|
fail('Should fail');
|
|
} catch (e) {
|
|
expect(e.code).toEqual(142);
|
|
expect(e.message).toEqual('newRequiredField is required');
|
|
}
|
|
obj.set('newRequiredField', 'some value2');
|
|
await obj.save();
|
|
expect(obj.get('newRequiredField')).toEqual('some value2');
|
|
expect(obj.get('newRequiredFieldWithDefaultValue')).toEqual('some value');
|
|
expect(obj.get('newNotRequiredField')).toEqual(undefined);
|
|
expect(obj.get('newNotRequiredFieldWithDefaultValue')).toEqual('some value');
|
|
expect(obj.get('newRegularField')).toEqual(undefined);
|
|
obj.unset('newRequiredFieldWithDefaultValue');
|
|
try {
|
|
await obj.save();
|
|
fail('Should fail');
|
|
} catch (e) {
|
|
expect(e.code).toEqual(142);
|
|
expect(e.message).toEqual('newRequiredFieldWithDefaultValue is required');
|
|
}
|
|
obj.set('newRequiredFieldWithDefaultValue', '');
|
|
try {
|
|
await obj.save();
|
|
fail('Should fail');
|
|
} catch (e) {
|
|
expect(e.code).toEqual(142);
|
|
expect(e.message).toEqual('newRequiredFieldWithDefaultValue is required');
|
|
}
|
|
obj.set('newRequiredFieldWithDefaultValue', 'some value2');
|
|
obj.set('newNotRequiredField', '');
|
|
obj.set('newNotRequiredFieldWithDefaultValue', null);
|
|
obj.unset('newRegularField');
|
|
await obj.save();
|
|
expect(obj.get('newRequiredField')).toEqual('some value2');
|
|
expect(obj.get('newRequiredFieldWithDefaultValue')).toEqual('some value2');
|
|
expect(obj.get('newNotRequiredField')).toEqual('');
|
|
expect(obj.get('newNotRequiredFieldWithDefaultValue')).toEqual(null);
|
|
expect(obj.get('newRegularField')).toEqual(undefined);
|
|
obj = new Parse.Object('NewClass');
|
|
obj.set('newRequiredField', 'some value3');
|
|
obj.set('newRequiredFieldWithDefaultValue', 'some value3');
|
|
obj.set('newNotRequiredField', 'some value3');
|
|
obj.set('newNotRequiredFieldWithDefaultValue', 'some value3');
|
|
obj.set('newRegularField', 'some value3');
|
|
await obj.save();
|
|
expect(obj.get('newRequiredField')).toEqual('some value3');
|
|
expect(obj.get('newRequiredFieldWithDefaultValue')).toEqual('some value3');
|
|
expect(obj.get('newNotRequiredField')).toEqual('some value3');
|
|
expect(obj.get('newNotRequiredFieldWithDefaultValue')).toEqual('some value3');
|
|
expect(obj.get('newRegularField')).toEqual('some value3');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should validate required fields and set default values after before save trigger', async () => {
|
|
await request({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'NewClassForBeforeSaveTest',
|
|
fields: {
|
|
foo1: { type: 'String' },
|
|
foo2: { type: 'String', required: true },
|
|
foo3: {
|
|
type: 'String',
|
|
required: true,
|
|
defaultValue: 'some default value 3',
|
|
},
|
|
foo4: { type: 'String', defaultValue: 'some default value 4' },
|
|
},
|
|
},
|
|
});
|
|
|
|
Parse.Cloud.beforeSave('NewClassForBeforeSaveTest', req => {
|
|
req.object.set('foo1', 'some value 1');
|
|
req.object.set('foo2', 'some value 2');
|
|
req.object.set('foo3', 'some value 3');
|
|
req.object.set('foo4', 'some value 4');
|
|
});
|
|
|
|
let obj = new Parse.Object('NewClassForBeforeSaveTest');
|
|
await obj.save();
|
|
|
|
expect(obj.get('foo1')).toEqual('some value 1');
|
|
expect(obj.get('foo2')).toEqual('some value 2');
|
|
expect(obj.get('foo3')).toEqual('some value 3');
|
|
expect(obj.get('foo4')).toEqual('some value 4');
|
|
|
|
Parse.Cloud.beforeSave('NewClassForBeforeSaveTest', req => {
|
|
req.object.set('foo1', 'some value 1');
|
|
req.object.set('foo2', 'some value 2');
|
|
});
|
|
|
|
obj = new Parse.Object('NewClassForBeforeSaveTest');
|
|
await obj.save();
|
|
|
|
expect(obj.get('foo1')).toEqual('some value 1');
|
|
expect(obj.get('foo2')).toEqual('some value 2');
|
|
expect(obj.get('foo3')).toEqual('some default value 3');
|
|
expect(obj.get('foo4')).toEqual('some default value 4');
|
|
|
|
Parse.Cloud.beforeSave('NewClassForBeforeSaveTest', req => {
|
|
req.object.set('foo1', 'some value 1');
|
|
req.object.set('foo2', 'some value 2');
|
|
req.object.set('foo3', undefined);
|
|
req.object.unset('foo4');
|
|
});
|
|
|
|
obj = new Parse.Object('NewClassForBeforeSaveTest');
|
|
obj.set('foo3', 'some value 3');
|
|
obj.set('foo4', 'some value 4');
|
|
await obj.save();
|
|
|
|
expect(obj.get('foo1')).toEqual('some value 1');
|
|
expect(obj.get('foo2')).toEqual('some value 2');
|
|
expect(obj.get('foo3')).toEqual('some default value 3');
|
|
expect(obj.get('foo4')).toEqual('some default value 4');
|
|
|
|
Parse.Cloud.beforeSave('NewClassForBeforeSaveTest', req => {
|
|
req.object.set('foo1', 'some value 1');
|
|
req.object.set('foo2', undefined);
|
|
req.object.set('foo3', undefined);
|
|
req.object.unset('foo4');
|
|
});
|
|
|
|
obj = new Parse.Object('NewClassForBeforeSaveTest');
|
|
obj.set('foo2', 'some value 2');
|
|
obj.set('foo3', 'some value 3');
|
|
obj.set('foo4', 'some value 4');
|
|
|
|
try {
|
|
await obj.save();
|
|
fail('should fail');
|
|
} catch (e) {
|
|
expect(e.message).toEqual('foo2 is required');
|
|
}
|
|
|
|
Parse.Cloud.beforeSave('NewClassForBeforeSaveTest', req => {
|
|
req.object.set('foo1', 'some value 1');
|
|
req.object.unset('foo2');
|
|
req.object.set('foo3', undefined);
|
|
req.object.unset('foo4');
|
|
});
|
|
|
|
obj = new Parse.Object('NewClassForBeforeSaveTest');
|
|
obj.set('foo2', 'some value 2');
|
|
obj.set('foo3', 'some value 3');
|
|
obj.set('foo4', 'some value 4');
|
|
|
|
try {
|
|
await obj.save();
|
|
fail('should fail');
|
|
} catch (e) {
|
|
expect(e.message).toEqual('foo2 is required');
|
|
}
|
|
});
|
|
|
|
it('lets you add fields to system schema', done => {
|
|
request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/schemas/_User',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
}).then(fail, () => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/_User',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
fields: {
|
|
newField: { type: 'String' },
|
|
},
|
|
},
|
|
}).then(response => {
|
|
delete response.data.indexes;
|
|
expect(
|
|
dd(response.data, {
|
|
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,
|
|
protectedFields: {
|
|
'*': ['email'],
|
|
},
|
|
},
|
|
})
|
|
).toBeUndefined();
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/_User',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
}).then(response => {
|
|
delete response.data.indexes;
|
|
expect(
|
|
dd(response.data, {
|
|
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('lets you delete multiple fields and check schema', done => {
|
|
const simpleOneObject = () => {
|
|
const obj = new Parse.Object('SimpleOne');
|
|
obj.set('aNumber', 5);
|
|
obj.set('aString', 'string');
|
|
obj.set('aBool', true);
|
|
return obj;
|
|
};
|
|
|
|
simpleOneObject()
|
|
.save()
|
|
.then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/SimpleOne',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
fields: {
|
|
aString: { __op: 'Delete' },
|
|
aNumber: { __op: 'Delete' },
|
|
},
|
|
},
|
|
}).then(response => {
|
|
expect(response.data).toEqual({
|
|
className: 'SimpleOne',
|
|
fields: {
|
|
//Default fields
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
updatedAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
//Custom fields
|
|
aBool: { type: 'Boolean' },
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
});
|
|
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('lets you delete multiple fields and add fields', done => {
|
|
const obj1 = hasAllPODobject();
|
|
obj1.save().then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/HasAllPOD',
|
|
method: 'PUT',
|
|
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' },
|
|
},
|
|
},
|
|
}).then(response => {
|
|
expect(response.data).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,
|
|
});
|
|
const obj2 = new Parse.Object('HasAllPOD');
|
|
obj2.set('aNewPointer', obj1);
|
|
const 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 => {
|
|
const obj = hasAllPODobject();
|
|
obj.save().then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/HasAllPOD',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
fields: {
|
|
fakeNewField: { type: 'fake type' },
|
|
aString: { __op: 'Delete' },
|
|
},
|
|
},
|
|
}).then(fail, response => {
|
|
expect(response.data.code).toEqual(Parse.Error.INCORRECT_TYPE);
|
|
expect(response.data.error).toEqual('invalid field type: fake type');
|
|
request({
|
|
method: 'PUT',
|
|
url: 'http://localhost:8378/1/schemas/HasAllPOD',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
}).then(response => {
|
|
expect(response.data).toEqual(plainOldDataSchema);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('requires the master key to delete schemas', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/DoesntMatter',
|
|
method: 'DELETE',
|
|
headers: noAuthHeaders,
|
|
json: true,
|
|
}).then(fail, response => {
|
|
expect(response.status).toEqual(403);
|
|
expect(response.data.error).toEqual('unauthorized');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('refuses to delete non-empty collection', done => {
|
|
const obj = hasAllPODobject();
|
|
obj.save().then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/HasAllPOD',
|
|
method: 'DELETE',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
}).then(fail, response => {
|
|
expect(response.status).toEqual(400);
|
|
expect(response.data.code).toEqual(255);
|
|
expect(response.data.error).toMatch(/HasAllPOD/);
|
|
expect(response.data.error).toMatch(/contains 1/);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('fails when deleting collections with invalid class names', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/_GlobalConfig',
|
|
method: 'DELETE',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
}).then(fail, response => {
|
|
expect(response.status).toEqual(400);
|
|
expect(response.data.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
|
expect(response.data.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({
|
|
url: 'http://localhost:8378/1/schemas/Missing',
|
|
method: 'DELETE',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
}).then(response => {
|
|
expect(response.status).toEqual(200);
|
|
expect(response.data).toEqual({});
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('ensure refresh cache after deleting a class', async done => {
|
|
config = Config.get('test');
|
|
spyOn(config.schemaCache, 'del').and.callFake(() => {});
|
|
spyOn(SchemaController.prototype, 'reloadData').and.callFake(() => Promise.resolve());
|
|
await request({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'A',
|
|
},
|
|
});
|
|
await request({
|
|
method: 'DELETE',
|
|
url: 'http://localhost:8378/1/schemas/A',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
});
|
|
const response = await request({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
method: 'GET',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
});
|
|
const expected = {
|
|
results: [userSchema, roleSchema],
|
|
};
|
|
expect(
|
|
response.data.results
|
|
.sort((s1, s2) => s1.className.localeCompare(s2.className))
|
|
.map(s => {
|
|
const withoutIndexes = Object.assign({}, s);
|
|
delete withoutIndexes.indexes;
|
|
return withoutIndexes;
|
|
})
|
|
).toEqual(expected.results.sort((s1, s2) => s1.className.localeCompare(s2.className)));
|
|
done();
|
|
});
|
|
|
|
it('deletes collections including join tables', done => {
|
|
const obj = new Parse.Object('MyClass');
|
|
obj.set('data', 'data');
|
|
obj
|
|
.save()
|
|
.then(() => {
|
|
const obj2 = new Parse.Object('MyOtherClass');
|
|
const relation = obj2.relation('aRelation');
|
|
relation.add(obj);
|
|
return obj2.save();
|
|
})
|
|
.then(obj2 => obj2.destroy())
|
|
.then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/MyOtherClass',
|
|
method: 'DELETE',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
}).then(response => {
|
|
expect(response.status).toEqual(200);
|
|
expect(response.data).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({
|
|
url: 'http://localhost:8378/1/schemas/MyOtherClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
}).then(fail, response => {
|
|
//Expect _SCHEMA entry to be gone.
|
|
expect(response.status).toEqual(400);
|
|
expect(response.data.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
|
expect(response.data.error).toEqual('Class MyOtherClass does not exist.');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
})
|
|
.then(
|
|
() => {},
|
|
error => {
|
|
fail(error);
|
|
done();
|
|
}
|
|
);
|
|
});
|
|
|
|
it('deletes schema when actual collection does not exist', done => {
|
|
request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/schemas/NewClassForDelete',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'NewClassForDelete',
|
|
},
|
|
}).then(response => {
|
|
expect(response.data.className).toEqual('NewClassForDelete');
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClassForDelete',
|
|
method: 'DELETE',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
}).then(response => {
|
|
expect(response.status).toEqual(200);
|
|
expect(response.data).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({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/schemas/NewClassForDelete',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'NewClassForDelete',
|
|
},
|
|
}).then(response => {
|
|
expect(response.data.className).toEqual('NewClassForDelete');
|
|
request({
|
|
url: 'http://localhost:8378/1/classes/NewClassForDelete',
|
|
method: 'POST',
|
|
headers: restKeyHeaders,
|
|
json: true,
|
|
}).then(response => {
|
|
expect(typeof response.data.objectId).toEqual('string');
|
|
request({
|
|
method: 'DELETE',
|
|
url: 'http://localhost:8378/1/classes/NewClassForDelete/' + response.data.objectId,
|
|
headers: restKeyHeaders,
|
|
json: true,
|
|
}).then(() => {
|
|
request({
|
|
method: 'DELETE',
|
|
url: 'http://localhost:8378/1/schemas/NewClassForDelete',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
}).then(response => {
|
|
expect(response.status).toEqual(200);
|
|
expect(response.data).toEqual({});
|
|
config.database.loadSchema().then(schema => {
|
|
schema.hasClass('NewClassForDelete').then(exist => {
|
|
expect(exist).toEqual(false);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should set/get schema permissions', done => {
|
|
request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/schemas/AClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
classLevelPermissions: {
|
|
find: {
|
|
'*': true,
|
|
},
|
|
create: {
|
|
'role:admin': true,
|
|
},
|
|
},
|
|
},
|
|
}).then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/AClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
}).then(response => {
|
|
expect(response.status).toEqual(200);
|
|
expect(response.data.classLevelPermissions).toEqual({
|
|
find: {
|
|
'*': true,
|
|
},
|
|
create: {
|
|
'role:admin': true,
|
|
},
|
|
get: {},
|
|
count: {},
|
|
update: {},
|
|
delete: {},
|
|
addField: {},
|
|
protectedFields: {},
|
|
});
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should fail setting schema permissions with invalid key', done => {
|
|
const object = new Parse.Object('AClass');
|
|
object.save().then(() => {
|
|
request({
|
|
method: 'PUT',
|
|
url: 'http://localhost:8378/1/schemas/AClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
classLevelPermissions: {
|
|
find: {
|
|
'*': true,
|
|
},
|
|
create: {
|
|
'role:admin': true,
|
|
},
|
|
dummy: {
|
|
some: true,
|
|
},
|
|
},
|
|
},
|
|
}).then(fail, response => {
|
|
expect(response.data.code).toEqual(107);
|
|
expect(response.data.error).toEqual(
|
|
'dummy is not a valid operation for class level permissions'
|
|
);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should not be able to add a field', done => {
|
|
request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/schemas/AClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
classLevelPermissions: {
|
|
create: {
|
|
'*': true,
|
|
},
|
|
find: {
|
|
'*': true,
|
|
},
|
|
addField: {
|
|
'role:admin': true,
|
|
},
|
|
},
|
|
},
|
|
}).then(() => {
|
|
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({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/schemas/AClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
classLevelPermissions: {
|
|
create: {
|
|
'*': true,
|
|
},
|
|
addField: {
|
|
'*': true,
|
|
},
|
|
},
|
|
},
|
|
}).then(() => {
|
|
const object = new Parse.Object('AClass');
|
|
object.set('hello', 'world');
|
|
return object.save().then(
|
|
() => {
|
|
done();
|
|
},
|
|
() => {
|
|
fail('should be able to add a field');
|
|
done();
|
|
}
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Nested documents', () => {
|
|
beforeAll(async () => {
|
|
const testSchema = new Parse.Schema('test_7371');
|
|
testSchema.setCLP({
|
|
create: { ['*']: true },
|
|
update: { ['*']: true },
|
|
addField: {},
|
|
});
|
|
testSchema.addObject('a');
|
|
await testSchema.save();
|
|
});
|
|
|
|
it('addField permission not required for adding a nested property', async () => {
|
|
const obj = new Parse.Object('test_7371');
|
|
obj.set('a', {});
|
|
await obj.save();
|
|
obj.set('a.b', 2);
|
|
await obj.save();
|
|
});
|
|
it('addField permission not required for modifying a nested property', async () => {
|
|
const obj = new Parse.Object('test_7371');
|
|
obj.set('a', { b: 1 });
|
|
await obj.save();
|
|
obj.set('a.b', 2);
|
|
await obj.save();
|
|
});
|
|
});
|
|
|
|
it('should aceept class-level permission with userid of any length', async done => {
|
|
await global.reconfigureServer({
|
|
customIdSize: 11,
|
|
});
|
|
|
|
const id = 'e1evenChars';
|
|
|
|
const { data } = await request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/schemas/AClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
classLevelPermissions: {
|
|
find: {
|
|
[id]: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(data.classLevelPermissions.find[id]).toBe(true);
|
|
|
|
done();
|
|
});
|
|
|
|
it('should allow set class-level permission for custom userid of any length and chars', async done => {
|
|
await global.reconfigureServer({
|
|
allowCustomObjectId: true,
|
|
});
|
|
|
|
const symbolsId = 'set:ID+symbol$=@llowed';
|
|
const shortId = '1';
|
|
const { data } = await request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/schemas/AClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
classLevelPermissions: {
|
|
find: {
|
|
[symbolsId]: true,
|
|
[shortId]: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(data.classLevelPermissions.find[symbolsId]).toBe(true);
|
|
expect(data.classLevelPermissions.find[shortId]).toBe(true);
|
|
|
|
done();
|
|
});
|
|
|
|
it('should allow set ACL for custom userid', async done => {
|
|
await global.reconfigureServer({
|
|
allowCustomObjectId: true,
|
|
});
|
|
|
|
const symbolsId = 'symbols:id@allowed=';
|
|
const shortId = '1';
|
|
const normalId = 'tensymbols';
|
|
|
|
const { data } = await request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/classes/AClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
ACL: {
|
|
[symbolsId]: { read: true, write: true },
|
|
[shortId]: { read: true, write: true },
|
|
[normalId]: { read: true, write: true },
|
|
},
|
|
},
|
|
});
|
|
|
|
const { data: created } = await request({
|
|
method: 'GET',
|
|
url: `http://localhost:8378/1/classes/AClass/${data.objectId}`,
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
});
|
|
|
|
expect(created.ACL[normalId].write).toBe(true);
|
|
expect(created.ACL[symbolsId].write).toBe(true);
|
|
expect(created.ACL[shortId].write).toBe(true);
|
|
done();
|
|
});
|
|
|
|
it('should throw with invalid userId (invalid char)', done => {
|
|
request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/schemas/AClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
classLevelPermissions: {
|
|
find: {
|
|
'12345_6789': true,
|
|
},
|
|
},
|
|
},
|
|
}).then(fail, response => {
|
|
expect(response.data.error).toEqual(
|
|
"'12345_6789' is not a valid key for class level permissions"
|
|
);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should throw with invalid * (spaces before)', done => {
|
|
request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/schemas/AClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
classLevelPermissions: {
|
|
find: {
|
|
' *': true,
|
|
},
|
|
},
|
|
},
|
|
}).then(fail, response => {
|
|
expect(response.data.error).toEqual("' *' is not a valid key for class level permissions");
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should throw with invalid * (spaces after)', done => {
|
|
request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/schemas/AClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
classLevelPermissions: {
|
|
find: {
|
|
'* ': true,
|
|
},
|
|
},
|
|
},
|
|
}).then(fail, response => {
|
|
expect(response.data.error).toEqual("'* ' is not a valid key for class level permissions");
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should throw if permission is number', done => {
|
|
request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/schemas/AClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
classLevelPermissions: {
|
|
find: {
|
|
'*': 1,
|
|
},
|
|
},
|
|
},
|
|
}).then(fail, response => {
|
|
expect(response.data.error).toEqual(
|
|
"'1' is not a valid value for class level permissions find:*:1"
|
|
);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should throw if permission is empty string', done => {
|
|
request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/schemas/AClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
classLevelPermissions: {
|
|
find: {
|
|
'*': '',
|
|
},
|
|
},
|
|
},
|
|
}).then(fail, response => {
|
|
expect(response.data.error).toEqual(
|
|
"'' is not a valid value for class level permissions find:*:"
|
|
);
|
|
done();
|
|
});
|
|
});
|
|
|
|
function setPermissionsOnClass(className, permissions, doPut) {
|
|
return request({
|
|
url: 'http://localhost:8378/1/schemas/' + className,
|
|
method: doPut ? 'PUT' : 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
classLevelPermissions: permissions,
|
|
},
|
|
}).then(response => {
|
|
if (response.data.error) {
|
|
throw response.data;
|
|
}
|
|
return response.data;
|
|
});
|
|
}
|
|
|
|
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(() => {
|
|
const 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('unset field in beforeSave should not stop object creation', done => {
|
|
const hook = {
|
|
method: function (req) {
|
|
if (req.object.get('undesiredField')) {
|
|
req.object.unset('undesiredField');
|
|
}
|
|
},
|
|
};
|
|
spyOn(hook, 'method').and.callThrough();
|
|
Parse.Cloud.beforeSave('AnObject', hook.method);
|
|
setPermissionsOnClass('AnObject', {
|
|
get: { '*': true },
|
|
find: { '*': true },
|
|
create: { '*': true },
|
|
update: { '*': true },
|
|
delete: { '*': true },
|
|
addField: {},
|
|
})
|
|
.then(() => {
|
|
const obj = new Parse.Object('AnObject');
|
|
obj.set('desiredField', 'createMe');
|
|
return obj.save(null, { useMasterKey: true });
|
|
})
|
|
.then(() => {
|
|
const obj = new Parse.Object('AnObject');
|
|
obj.set('desiredField', 'This value should be kept');
|
|
obj.set('undesiredField', 'This value should be IGNORED');
|
|
return obj.save();
|
|
})
|
|
.then(() => {
|
|
const query = new Parse.Query('AnObject');
|
|
return query.find();
|
|
})
|
|
.then(results => {
|
|
expect(results.length).toBe(2);
|
|
expect(results[0].has('desiredField')).toBe(true);
|
|
expect(results[1].has('desiredField')).toBe(true);
|
|
expect(results[0].has('undesiredField')).toBe(false);
|
|
expect(results[1].has('undesiredField')).toBe(false);
|
|
expect(hook.method).toHaveBeenCalled();
|
|
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({
|
|
method: 'DELETE',
|
|
url: 'http://localhost:8378/1/schemas/MyClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
}).then(response => {
|
|
expect(response.status).toEqual(200);
|
|
expect(response.data).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 #4409 (indexes override the clp)', done => {
|
|
setPermissionsOnClass(
|
|
'_Role',
|
|
{
|
|
get: { '*': true },
|
|
find: { '*': true },
|
|
count: { '*': true },
|
|
create: { '*': true },
|
|
},
|
|
true
|
|
)
|
|
.then(() => {
|
|
const config = Config.get('test');
|
|
return config.database.adapter.updateSchemaWithIndexes();
|
|
})
|
|
.then(() => {
|
|
return request({
|
|
url: 'http://localhost:8378/1/schemas/_Role',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
});
|
|
})
|
|
.then(res => {
|
|
expect(res.data.classLevelPermissions).toEqual({
|
|
get: { '*': true },
|
|
find: { '*': true },
|
|
count: { '*': true },
|
|
create: { '*': true },
|
|
update: {},
|
|
delete: {},
|
|
addField: {},
|
|
protectedFields: {},
|
|
});
|
|
})
|
|
.then(done)
|
|
.catch(done.fail);
|
|
});
|
|
|
|
it('regression test for #5177', async () => {
|
|
Parse.Object.disableSingleInstance();
|
|
Parse.Cloud.beforeSave('AClass', () => {});
|
|
await setPermissionsOnClass(
|
|
'AClass',
|
|
{
|
|
update: { '*': true },
|
|
},
|
|
false
|
|
);
|
|
const obj = new Parse.Object('AClass');
|
|
await obj.save({ key: 1 }, { useMasterKey: true });
|
|
obj.increment('key', 10);
|
|
const objectAgain = await obj.save();
|
|
expect(objectAgain.get('key')).toBe(11);
|
|
});
|
|
|
|
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();
|
|
}
|
|
);
|
|
});
|
|
|
|
it('should reject creating class schema with field with invalid key', async done => {
|
|
const config = Config.get(Parse.applicationId);
|
|
const schemaController = await config.database.loadSchema();
|
|
|
|
const fieldName = '1invalid';
|
|
|
|
const schemaCreation = () =>
|
|
schemaController.addClassIfNotExists('AnObject', {
|
|
[fieldName]: { __type: 'String' },
|
|
});
|
|
|
|
await expectAsync(schemaCreation()).toBeRejectedWith(
|
|
new Parse.Error(Parse.Error.INVALID_KEY_NAME, `invalid field name: ${fieldName}`)
|
|
);
|
|
done();
|
|
});
|
|
|
|
it('should reject creating invalid field name', async done => {
|
|
const object = new Parse.Object('AnObject');
|
|
|
|
await expectAsync(
|
|
object.save({
|
|
'!12field': 'field',
|
|
})
|
|
).toBeRejectedWith(new Parse.Error(Parse.Error.INVALID_KEY_NAME));
|
|
done();
|
|
});
|
|
|
|
it('should be rejected if CLP operation is not an object', async done => {
|
|
const config = Config.get(Parse.applicationId);
|
|
const schemaController = await config.database.loadSchema();
|
|
|
|
const operationKey = 'get';
|
|
const operation = true;
|
|
|
|
const schemaSetup = async () =>
|
|
await schemaController.addClassIfNotExists(
|
|
'AnObject',
|
|
{},
|
|
{
|
|
[operationKey]: operation,
|
|
}
|
|
);
|
|
|
|
await expectAsync(schemaSetup()).toBeRejectedWith(
|
|
new Parse.Error(
|
|
Parse.Error.INVALID_JSON,
|
|
`'${operation}' is not a valid value for class level permissions ${operationKey} - must be an object`
|
|
)
|
|
);
|
|
|
|
done();
|
|
});
|
|
|
|
it('should be rejected if CLP protectedFields is not an object', async done => {
|
|
const config = Config.get(Parse.applicationId);
|
|
const schemaController = await config.database.loadSchema();
|
|
|
|
const operationKey = 'get';
|
|
const operation = 'wrongtype';
|
|
|
|
const schemaSetup = async () =>
|
|
await schemaController.addClassIfNotExists(
|
|
'AnObject',
|
|
{},
|
|
{
|
|
[operationKey]: operation,
|
|
}
|
|
);
|
|
|
|
await expectAsync(schemaSetup()).toBeRejectedWith(
|
|
new Parse.Error(
|
|
Parse.Error.INVALID_JSON,
|
|
`'${operation}' is not a valid value for class level permissions ${operationKey} - must be an object`
|
|
)
|
|
);
|
|
|
|
done();
|
|
});
|
|
|
|
it('should be rejected if CLP read/writeUserFields is not an array', async done => {
|
|
const config = Config.get(Parse.applicationId);
|
|
const schemaController = await config.database.loadSchema();
|
|
|
|
const operationKey = 'readUserFields';
|
|
const operation = true;
|
|
|
|
const schemaSetup = async () =>
|
|
await schemaController.addClassIfNotExists(
|
|
'AnObject',
|
|
{},
|
|
{
|
|
[operationKey]: operation,
|
|
}
|
|
);
|
|
|
|
await expectAsync(schemaSetup()).toBeRejectedWith(
|
|
new Parse.Error(
|
|
Parse.Error.INVALID_JSON,
|
|
`'${operation}' is not a valid value for class level permissions ${operationKey} - must be an array`
|
|
)
|
|
);
|
|
|
|
done();
|
|
});
|
|
|
|
it('should be rejected if CLP pointerFields is not an array', async done => {
|
|
const config = Config.get(Parse.applicationId);
|
|
const schemaController = await config.database.loadSchema();
|
|
|
|
const operationKey = 'get';
|
|
const entity = 'pointerFields';
|
|
const value = {};
|
|
|
|
const schemaSetup = async () =>
|
|
await schemaController.addClassIfNotExists(
|
|
'AnObject',
|
|
{},
|
|
{
|
|
[operationKey]: {
|
|
[entity]: value,
|
|
},
|
|
}
|
|
);
|
|
|
|
await expectAsync(schemaSetup()).toBeRejectedWith(
|
|
new Parse.Error(
|
|
Parse.Error.INVALID_JSON,
|
|
`'${value}' is not a valid value for ${operationKey}[${entity}] - expected an array.`
|
|
)
|
|
);
|
|
|
|
done();
|
|
});
|
|
|
|
describe('index management', () => {
|
|
beforeEach(async () => {
|
|
await TestUtils.destroyAllDataPermanently(false);
|
|
await config.database.adapter.performInitialization({ VolatileClassesSchemas: [] });
|
|
});
|
|
|
|
it('cannot create index if field does not exist', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {},
|
|
}).then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
indexes: {
|
|
name1: { aString: 1 },
|
|
},
|
|
},
|
|
}).then(fail, response => {
|
|
expect(response.data.code).toBe(Parse.Error.INVALID_QUERY);
|
|
expect(response.data.error).toBe('Field aString does not exist, cannot add index.');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('can create index on default field', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {},
|
|
}).then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
indexes: {
|
|
name1: { createdAt: 1 },
|
|
},
|
|
},
|
|
}).then(response => {
|
|
expect(response.data.indexes.name1).toEqual({ createdAt: 1 });
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('cannot create compound index if field does not exist', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {},
|
|
}).then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
fields: {
|
|
aString: { type: 'String' },
|
|
},
|
|
indexes: {
|
|
name1: { aString: 1, bString: 1 },
|
|
},
|
|
},
|
|
}).then(fail, response => {
|
|
expect(response.data.code).toBe(Parse.Error.INVALID_QUERY);
|
|
expect(response.data.error).toBe('Field bString does not exist, cannot add index.');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('allows add index when you create a class', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'NewClass',
|
|
fields: {
|
|
aString: { type: 'String' },
|
|
},
|
|
indexes: {
|
|
name1: { aString: 1 },
|
|
},
|
|
},
|
|
}).then(response => {
|
|
expect(response.data).toEqual({
|
|
className: 'NewClass',
|
|
fields: {
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
updatedAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
aString: { type: 'String' },
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
indexes: {
|
|
name1: { aString: 1 },
|
|
},
|
|
});
|
|
config.database.adapter.getIndexes('NewClass').then(indexes => {
|
|
expect(indexes.length).toBe(2);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('empty index returns nothing', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
className: 'NewClass',
|
|
fields: {
|
|
aString: { type: 'String' },
|
|
},
|
|
indexes: {},
|
|
},
|
|
}).then(response => {
|
|
expect(response.data).toEqual({
|
|
className: 'NewClass',
|
|
fields: {
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
updatedAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
aString: { type: 'String' },
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
});
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('lets you add indexes', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {},
|
|
}).then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
fields: {
|
|
aString: { type: 'String' },
|
|
},
|
|
indexes: {
|
|
name1: { aString: 1 },
|
|
},
|
|
},
|
|
}).then(response => {
|
|
expect(
|
|
dd(response.data, {
|
|
className: 'NewClass',
|
|
fields: {
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
updatedAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
aString: { type: 'String' },
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
indexes: {
|
|
_id_: { _id: 1 },
|
|
name1: { aString: 1 },
|
|
},
|
|
})
|
|
).toEqual(undefined);
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
}).then(response => {
|
|
expect(response.data).toEqual({
|
|
className: 'NewClass',
|
|
fields: {
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
updatedAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
aString: { type: 'String' },
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
indexes: {
|
|
_id_: { _id: 1 },
|
|
name1: { aString: 1 },
|
|
},
|
|
});
|
|
config.database.adapter.getIndexes('NewClass').then(indexes => {
|
|
expect(indexes.length).toEqual(2);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it_only_db('mongo')('lets you add index with with pointer like structure', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {},
|
|
}).then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
fields: {
|
|
aPointer: { type: 'Pointer', targetClass: 'NewClass' },
|
|
},
|
|
indexes: {
|
|
pointer: { _p_aPointer: 1 },
|
|
},
|
|
},
|
|
}).then(response => {
|
|
expect(
|
|
dd(response.data, {
|
|
className: 'NewClass',
|
|
fields: {
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
updatedAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
aPointer: { type: 'Pointer', targetClass: 'NewClass' },
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
indexes: {
|
|
_id_: { _id: 1 },
|
|
pointer: { _p_aPointer: 1 },
|
|
},
|
|
})
|
|
).toEqual(undefined);
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
}).then(response => {
|
|
expect(response.data).toEqual({
|
|
className: 'NewClass',
|
|
fields: {
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
updatedAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
aPointer: { type: 'Pointer', targetClass: 'NewClass' },
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
indexes: {
|
|
_id_: { _id: 1 },
|
|
pointer: { _p_aPointer: 1 },
|
|
},
|
|
});
|
|
config.database.adapter.getIndexes('NewClass').then(indexes => {
|
|
expect(indexes.length).toEqual(2);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('lets you add multiple indexes', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {},
|
|
}).then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
fields: {
|
|
aString: { type: 'String' },
|
|
bString: { type: 'String' },
|
|
cString: { type: 'String' },
|
|
dString: { type: 'String' },
|
|
},
|
|
indexes: {
|
|
name1: { aString: 1 },
|
|
name2: { bString: 1 },
|
|
name3: { cString: 1, dString: 1 },
|
|
},
|
|
},
|
|
}).then(response => {
|
|
expect(
|
|
dd(response.data, {
|
|
className: 'NewClass',
|
|
fields: {
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
updatedAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
aString: { type: 'String' },
|
|
bString: { type: 'String' },
|
|
cString: { type: 'String' },
|
|
dString: { type: 'String' },
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
indexes: {
|
|
_id_: { _id: 1 },
|
|
name1: { aString: 1 },
|
|
name2: { bString: 1 },
|
|
name3: { cString: 1, dString: 1 },
|
|
},
|
|
})
|
|
).toEqual(undefined);
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
}).then(response => {
|
|
expect(response.data).toEqual({
|
|
className: 'NewClass',
|
|
fields: {
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
updatedAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
aString: { type: 'String' },
|
|
bString: { type: 'String' },
|
|
cString: { type: 'String' },
|
|
dString: { type: 'String' },
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
indexes: {
|
|
_id_: { _id: 1 },
|
|
name1: { aString: 1 },
|
|
name2: { bString: 1 },
|
|
name3: { cString: 1, dString: 1 },
|
|
},
|
|
});
|
|
config.database.adapter.getIndexes('NewClass').then(indexes => {
|
|
expect(indexes.length).toEqual(4);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('lets you delete indexes', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {},
|
|
}).then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
fields: {
|
|
aString: { type: 'String' },
|
|
},
|
|
indexes: {
|
|
name1: { aString: 1 },
|
|
},
|
|
},
|
|
}).then(response => {
|
|
expect(
|
|
dd(response.data, {
|
|
className: 'NewClass',
|
|
fields: {
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
updatedAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
aString: { type: 'String' },
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
indexes: {
|
|
_id_: { _id: 1 },
|
|
name1: { aString: 1 },
|
|
},
|
|
})
|
|
).toEqual(undefined);
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
indexes: {
|
|
name1: { __op: 'Delete' },
|
|
},
|
|
},
|
|
}).then(response => {
|
|
expect(response.data).toEqual({
|
|
className: 'NewClass',
|
|
fields: {
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
updatedAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
aString: { type: 'String' },
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
indexes: {
|
|
_id_: { _id: 1 },
|
|
},
|
|
});
|
|
config.database.adapter.getIndexes('NewClass').then(indexes => {
|
|
expect(indexes.length).toEqual(1);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('lets you delete multiple indexes', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {},
|
|
}).then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
fields: {
|
|
aString: { type: 'String' },
|
|
bString: { type: 'String' },
|
|
cString: { type: 'String' },
|
|
},
|
|
indexes: {
|
|
name1: { aString: 1 },
|
|
name2: { bString: 1 },
|
|
name3: { cString: 1 },
|
|
},
|
|
},
|
|
}).then(response => {
|
|
expect(
|
|
dd(response.data, {
|
|
className: 'NewClass',
|
|
fields: {
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
updatedAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
aString: { type: 'String' },
|
|
bString: { type: 'String' },
|
|
cString: { type: 'String' },
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
indexes: {
|
|
_id_: { _id: 1 },
|
|
name1: { aString: 1 },
|
|
name2: { bString: 1 },
|
|
name3: { cString: 1 },
|
|
},
|
|
})
|
|
).toEqual(undefined);
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
indexes: {
|
|
name1: { __op: 'Delete' },
|
|
name2: { __op: 'Delete' },
|
|
},
|
|
},
|
|
}).then(response => {
|
|
expect(response.data).toEqual({
|
|
className: 'NewClass',
|
|
fields: {
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
updatedAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
aString: { type: 'String' },
|
|
bString: { type: 'String' },
|
|
cString: { type: 'String' },
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
indexes: {
|
|
_id_: { _id: 1 },
|
|
name3: { cString: 1 },
|
|
},
|
|
});
|
|
config.database.adapter.getIndexes('NewClass').then(indexes => {
|
|
expect(indexes.length).toEqual(2);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('lets you add and delete indexes', async () => {
|
|
// Wait due to index building in MongoDB on background process with collection lock
|
|
const waitForIndexBuild = new Promise(r => setTimeout(r, 500));
|
|
|
|
await request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {},
|
|
});
|
|
|
|
let response = await request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
fields: {
|
|
aString: { type: 'String' },
|
|
bString: { type: 'String' },
|
|
cString: { type: 'String' },
|
|
dString: { type: 'String' },
|
|
},
|
|
indexes: {
|
|
name1: { aString: 1 },
|
|
name2: { bString: 1 },
|
|
name3: { cString: 1 },
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(
|
|
dd(response.data, {
|
|
className: 'NewClass',
|
|
fields: {
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
updatedAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
aString: { type: 'String' },
|
|
bString: { type: 'String' },
|
|
cString: { type: 'String' },
|
|
dString: { type: 'String' },
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
indexes: {
|
|
_id_: { _id: 1 },
|
|
name1: { aString: 1 },
|
|
name2: { bString: 1 },
|
|
name3: { cString: 1 },
|
|
},
|
|
})
|
|
).toEqual(undefined);
|
|
|
|
await waitForIndexBuild;
|
|
response = await request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
indexes: {
|
|
name1: { __op: 'Delete' },
|
|
name2: { __op: 'Delete' },
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(response.data).toEqual({
|
|
className: 'NewClass',
|
|
fields: {
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
updatedAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
aString: { type: 'String' },
|
|
bString: { type: 'String' },
|
|
cString: { type: 'String' },
|
|
dString: { type: 'String' },
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
indexes: {
|
|
_id_: { _id: 1 },
|
|
name3: { cString: 1 },
|
|
},
|
|
});
|
|
|
|
await waitForIndexBuild;
|
|
response = await request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
indexes: {
|
|
name4: { dString: 1 },
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(response.data).toEqual({
|
|
className: 'NewClass',
|
|
fields: {
|
|
ACL: { type: 'ACL' },
|
|
createdAt: { type: 'Date' },
|
|
updatedAt: { type: 'Date' },
|
|
objectId: { type: 'String' },
|
|
aString: { type: 'String' },
|
|
bString: { type: 'String' },
|
|
cString: { type: 'String' },
|
|
dString: { type: 'String' },
|
|
},
|
|
classLevelPermissions: defaultClassLevelPermissions,
|
|
indexes: {
|
|
_id_: { _id: 1 },
|
|
name3: { cString: 1 },
|
|
name4: { dString: 1 },
|
|
},
|
|
});
|
|
|
|
await waitForIndexBuild;
|
|
const indexes = await config.database.adapter.getIndexes('NewClass');
|
|
expect(indexes.length).toEqual(3);
|
|
});
|
|
|
|
it('cannot delete index that does not exist', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {},
|
|
}).then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
indexes: {
|
|
unknownIndex: { __op: 'Delete' },
|
|
},
|
|
},
|
|
}).then(fail, response => {
|
|
expect(response.data.code).toBe(Parse.Error.INVALID_QUERY);
|
|
expect(response.data.error).toBe('Index unknownIndex does not exist, cannot delete.');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('cannot update index that exist', done => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'POST',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {},
|
|
}).then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
fields: {
|
|
aString: { type: 'String' },
|
|
},
|
|
indexes: {
|
|
name1: { aString: 1 },
|
|
},
|
|
},
|
|
}).then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
|
method: 'PUT',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
body: {
|
|
indexes: {
|
|
name1: { field2: 1 },
|
|
},
|
|
},
|
|
}).then(fail, response => {
|
|
expect(response.data.code).toBe(Parse.Error.INVALID_QUERY);
|
|
expect(response.data.error).toBe('Index name1 exists, cannot update.');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it_id('5d0926b2-2d31-459d-a2b1-23ecc32e72a3')(it_exclude_dbs(['postgres']))('get indexes on startup', done => {
|
|
const obj = new Parse.Object('TestObject');
|
|
obj
|
|
.save()
|
|
.then(() => {
|
|
return reconfigureServer({
|
|
appId: 'test',
|
|
restAPIKey: 'test',
|
|
publicServerURL: 'http://localhost:8378/1',
|
|
});
|
|
})
|
|
.then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/TestObject',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
}).then(response => {
|
|
expect(response.data.indexes._id_).toBeDefined();
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it_id('9f2ba51a-6a9c-4b25-9da0-51c82ac65f90')(it_exclude_dbs(['postgres']))('get compound indexes on startup', done => {
|
|
const obj = new Parse.Object('TestObject');
|
|
obj.set('subject', 'subject');
|
|
obj.set('comment', 'comment');
|
|
obj
|
|
.save()
|
|
.then(() => {
|
|
return config.database.adapter.createIndex('TestObject', {
|
|
subject: 'text',
|
|
comment: 'text',
|
|
});
|
|
})
|
|
.then(() => {
|
|
return reconfigureServer({
|
|
appId: 'test',
|
|
restAPIKey: 'test',
|
|
publicServerURL: 'http://localhost:8378/1',
|
|
});
|
|
})
|
|
.then(() => {
|
|
request({
|
|
url: 'http://localhost:8378/1/schemas/TestObject',
|
|
headers: masterKeyHeaders,
|
|
json: true,
|
|
}).then(response => {
|
|
expect(response.data.indexes._id_).toBeDefined();
|
|
expect(response.data.indexes._id_._id).toEqual(1);
|
|
expect(response.data.indexes.subject_text_comment_text).toBeDefined();
|
|
expect(response.data.indexes.subject_text_comment_text.subject).toEqual('text');
|
|
expect(response.data.indexes.subject_text_comment_text.comment).toEqual('text');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it_id('cbd5d897-b938-43a4-8f5a-5d02dd2be9be')(it_exclude_dbs(['postgres']))('cannot update to duplicate value on unique index', done => {
|
|
const index = {
|
|
code: 1,
|
|
};
|
|
const obj1 = new Parse.Object('UniqueIndexClass');
|
|
obj1.set('code', 1);
|
|
const obj2 = new Parse.Object('UniqueIndexClass');
|
|
obj2.set('code', 2);
|
|
const adapter = config.database.adapter;
|
|
adapter
|
|
._adaptiveCollection('UniqueIndexClass')
|
|
.then(collection => {
|
|
return collection._ensureSparseUniqueIndexInBackground(index);
|
|
})
|
|
.then(() => {
|
|
return obj1.save();
|
|
})
|
|
.then(() => {
|
|
return obj2.save();
|
|
})
|
|
.then(() => {
|
|
obj1.set('code', 2);
|
|
return obj1.save();
|
|
})
|
|
.then(done.fail)
|
|
.catch(error => {
|
|
expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|