Files
kami-parse-server/spec/DefinedSchemas.spec.js
2024-07-18 15:41:04 +02:00

711 lines
25 KiB
JavaScript

const { DefinedSchemas } = require('../lib/SchemaMigrations/DefinedSchemas');
const Config = require('../lib/Config');
const cleanUpIndexes = schema => {
if (schema.indexes) {
delete schema.indexes._id_;
if (!Object.keys(schema.indexes).length) {
delete schema.indexes;
}
}
};
describe('DefinedSchemas', () => {
let config;
afterEach(async () => {
config = Config.get('test');
if (config) {
await config.database.adapter.deleteAllClasses();
}
});
describe('Fields', () => {
it('should keep default fields if not provided', async () => {
const server = await reconfigureServer();
// Will perform create
await new DefinedSchemas({ definitions: [{ className: 'Test' }] }, server.config).execute();
let schema = await new Parse.Schema('Test').get();
const expectedFields = {
objectId: { type: 'String' },
createdAt: { type: 'Date' },
updatedAt: { type: 'Date' },
ACL: { type: 'ACL' },
};
expect(schema.fields).toEqual(expectedFields);
await server.config.schemaCache.clear();
// Will perform update
await new DefinedSchemas({ definitions: [{ className: 'Test' }] }, server.config).execute();
schema = await new Parse.Schema('Test').get();
expect(schema.fields).toEqual(expectedFields);
});
it('should protect default fields', async () => {
const server = await reconfigureServer();
const schemas = {
definitions: [
{
className: '_User',
fields: {
email: 'Object',
},
},
{
className: '_Role',
fields: {
users: 'Object',
},
},
{
className: '_Installation',
fields: {
installationId: 'Object',
},
},
{
className: 'Test',
fields: {
createdAt: { type: 'Object' },
objectId: { type: 'Number' },
updatedAt: { type: 'String' },
ACL: { type: 'String' },
},
},
],
};
const expectedFields = {
objectId: { type: 'String' },
createdAt: { type: 'Date' },
updatedAt: { type: 'Date' },
ACL: { type: 'ACL' },
};
const expectedUserFields = {
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' },
};
const expectedRoleFields = {
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' },
};
const expectedInstallationFields = {
objectId: { type: 'String' },
createdAt: { type: 'Date' },
updatedAt: { type: 'Date' },
ACL: { type: 'ACL' },
installationId: { type: 'String' },
deviceToken: { type: 'String' },
channels: { type: 'Array' },
deviceType: { type: 'String' },
pushType: { type: 'String' },
GCMSenderId: { type: 'String' },
timeZone: { type: 'String' },
localeIdentifier: { type: 'String' },
badge: { type: 'Number' },
appVersion: { type: 'String' },
appName: { type: 'String' },
appIdentifier: { type: 'String' },
parseVersion: { type: 'String' },
};
// Perform create
await new DefinedSchemas(schemas, server.config).execute();
let schema = await new Parse.Schema('Test').get();
expect(schema.fields).toEqual(expectedFields);
let userSchema = await new Parse.Schema('_User').get();
expect(userSchema.fields).toEqual(expectedUserFields);
let roleSchema = await new Parse.Schema('_Role').get();
expect(roleSchema.fields).toEqual(expectedRoleFields);
let installationSchema = await new Parse.Schema('_Installation').get();
expect(installationSchema.fields).toEqual(expectedInstallationFields);
await server.config.schemaCache.clear();
// Perform update
await new DefinedSchemas(schemas, server.config).execute();
schema = await new Parse.Schema('Test').get();
expect(schema.fields).toEqual(expectedFields);
userSchema = await new Parse.Schema('_User').get();
expect(userSchema.fields).toEqual(expectedUserFields);
roleSchema = await new Parse.Schema('_Role').get();
expect(roleSchema.fields).toEqual(expectedRoleFields);
installationSchema = await new Parse.Schema('_Installation').get();
expect(installationSchema.fields).toEqual(expectedInstallationFields);
});
it('should create new fields', async () => {
const server = await reconfigureServer();
const fields = {
objectId: { type: 'String' },
createdAt: { type: 'Date' },
updatedAt: { type: 'Date' },
ACL: { type: 'ACL' },
aString: { type: 'String' },
aStringWithDefault: { type: 'String', defaultValue: 'Test' },
aStringWithRequired: { type: 'String', required: true },
aStringWithRequiredAndDefault: { type: 'String', required: true, defaultValue: 'Test' },
aBoolean: { type: 'Boolean' },
aFile: { type: 'File' },
aNumber: { type: 'Number' },
aRelation: { type: 'Relation', targetClass: '_User' },
aPointer: { type: 'Pointer', targetClass: '_Role' },
aDate: { type: 'Date' },
aGeoPoint: { type: 'GeoPoint' },
aPolygon: { type: 'Polygon' },
aArray: { type: 'Array' },
aObject: { type: 'Object' },
};
const schemas = {
definitions: [
{
className: 'Test',
fields,
},
],
};
// Create
await new DefinedSchemas(schemas, server.config).execute();
let schema = await new Parse.Schema('Test').get();
expect(schema.fields).toEqual(fields);
fields.anotherObject = { type: 'Object' };
// Update
await new DefinedSchemas(schemas, server.config).execute();
schema = await new Parse.Schema('Test').get();
expect(schema.fields).toEqual(fields);
});
it('should not delete removed fields when "deleteExtraFields" is false', async () => {
const server = await reconfigureServer();
await new DefinedSchemas(
{ definitions: [{ className: 'Test', fields: { aField: { type: 'String' } } }] },
server.config
).execute();
let schema = await new Parse.Schema('Test').get();
expect(schema.fields.aField).toBeDefined();
await new DefinedSchemas({ definitions: [{ className: 'Test' }] }, server.config).execute();
schema = await new Parse.Schema('Test').get();
expect(schema.fields).toEqual({
objectId: { type: 'String' },
createdAt: { type: 'Date' },
updatedAt: { type: 'Date' },
aField: { type: 'String' },
ACL: { type: 'ACL' },
});
});
it('should delete removed fields when "deleteExtraFields" is true', async () => {
const server = await reconfigureServer();
await new DefinedSchemas(
{
definitions: [{ className: 'Test', fields: { aField: { type: 'String' } } }],
},
server.config
).execute();
let schema = await new Parse.Schema('Test').get();
expect(schema.fields.aField).toBeDefined();
await new DefinedSchemas(
{ deleteExtraFields: true, definitions: [{ className: 'Test' }] },
server.config
).execute();
schema = await new Parse.Schema('Test').get();
expect(schema.fields).toEqual({
objectId: { type: 'String' },
createdAt: { type: 'Date' },
updatedAt: { type: 'Date' },
ACL: { type: 'ACL' },
});
});
it('should re create fields with changed type when "recreateModifiedFields" is true', async () => {
const server = await reconfigureServer();
await new DefinedSchemas(
{ definitions: [{ className: 'Test', fields: { aField: { type: 'String' } } }] },
server.config
).execute();
let schema = await new Parse.Schema('Test').get();
expect(schema.fields.aField).toEqual({ type: 'String' });
const object = new Parse.Object('Test');
await object.save({ aField: 'Hello' }, { useMasterKey: true });
await new DefinedSchemas(
{
recreateModifiedFields: true,
definitions: [{ className: 'Test', fields: { aField: { type: 'Number' } } }],
},
server.config
).execute();
schema = await new Parse.Schema('Test').get();
expect(schema.fields.aField).toEqual({ type: 'Number' });
await object.fetch({ useMasterKey: true });
expect(object.get('aField')).toBeUndefined();
});
it('should not re create fields with changed type when "recreateModifiedFields" is not true', async () => {
const server = await reconfigureServer();
await new DefinedSchemas(
{ definitions: [{ className: 'Test', fields: { aField: { type: 'String' } } }] },
server.config
).execute();
let schema = await new Parse.Schema('Test').get();
expect(schema.fields.aField).toEqual({ type: 'String' });
const object = new Parse.Object('Test');
await object.save({ aField: 'Hello' }, { useMasterKey: true });
await new DefinedSchemas(
{ definitions: [{ className: 'Test', fields: { aField: { type: 'Number' } } }] },
server.config
).execute();
schema = await new Parse.Schema('Test').get();
expect(schema.fields.aField).toEqual({ type: 'String' });
await object.fetch({ useMasterKey: true });
expect(object.get('aField')).toBeDefined();
});
it('should just update classic fields with changed params', async () => {
const server = await reconfigureServer();
await new DefinedSchemas(
{ definitions: [{ className: 'Test', fields: { aField: { type: 'String' } } }] },
server.config
).execute();
let schema = await new Parse.Schema('Test').get();
expect(schema.fields.aField).toEqual({ type: 'String' });
const object = new Parse.Object('Test');
await object.save({ aField: 'Hello' }, { useMasterKey: true });
await new DefinedSchemas(
{
definitions: [
{ className: 'Test', fields: { aField: { type: 'String', required: true } } },
],
},
server.config
).execute();
schema = await new Parse.Schema('Test').get();
expect(schema.fields.aField).toEqual({ type: 'String', required: true });
await object.fetch({ useMasterKey: true });
expect(object.get('aField')).toEqual('Hello');
});
});
describe('Indexes', () => {
it('should create new indexes', async () => {
const server = await reconfigureServer();
const indexes = { complex: { createdAt: 1, updatedAt: 1 } };
const schemas = {
definitions: [{ className: 'Test', fields: { aField: { type: 'String' } }, indexes }],
};
await new DefinedSchemas(schemas, server.config).execute();
let schema = await new Parse.Schema('Test').get();
cleanUpIndexes(schema);
expect(schema.indexes).toEqual(indexes);
indexes.complex2 = { createdAt: 1, aField: 1 };
await new DefinedSchemas(schemas, server.config).execute();
schema = await new Parse.Schema('Test').get();
cleanUpIndexes(schema);
expect(schema.indexes).toEqual(indexes);
});
it('should re create changed indexes', async () => {
const server = await reconfigureServer();
let indexes = { complex: { createdAt: 1, updatedAt: 1 } };
let schemas = { definitions: [{ className: 'Test', indexes }] };
await new DefinedSchemas(schemas, server.config).execute();
indexes = { complex: { createdAt: 1 } };
schemas = { definitions: [{ className: 'Test', indexes }] };
// Change indexes
await new DefinedSchemas(schemas, server.config).execute();
let schema = await new Parse.Schema('Test').get();
cleanUpIndexes(schema);
expect(schema.indexes).toEqual(indexes);
// Update
await new DefinedSchemas(schemas, server.config).execute();
schema = await new Parse.Schema('Test').get();
cleanUpIndexes(schema);
expect(schema.indexes).toEqual(indexes);
});
it('should delete removed indexes', async () => {
const server = await reconfigureServer();
let indexes = { complex: { createdAt: 1, updatedAt: 1 } };
let schemas = { definitions: [{ className: 'Test', indexes }] };
await new DefinedSchemas(schemas, server.config).execute();
indexes = {};
schemas = { definitions: [{ className: 'Test', indexes }] };
// Change indexes
await new DefinedSchemas(schemas, server.config).execute();
let schema = await new Parse.Schema('Test').get();
cleanUpIndexes(schema);
expect(schema.indexes).toBeUndefined();
// Update
await new DefinedSchemas(schemas, server.config).execute();
schema = await new Parse.Schema('Test').get();
cleanUpIndexes(schema);
expect(schema.indexes).toBeUndefined();
});
xit('should keep protected indexes', async () => {
const server = await reconfigureServer();
const expectedIndexes = {
username_1: { username: 1 },
case_insensitive_username: { username: 1 },
email_1: { email: 1 },
case_insensitive_email: { email: 1 },
};
const schemas = {
definitions: [
{
className: '_User',
indexes: {
case_insensitive_username: { password: true },
case_insensitive_email: { password: true },
},
},
{ className: 'Test' },
],
};
// Create
await new DefinedSchemas(schemas, server.config).execute();
let userSchema = await new Parse.Schema('_User').get();
let testSchema = await new Parse.Schema('Test').get();
cleanUpIndexes(userSchema);
cleanUpIndexes(testSchema);
expect(testSchema.indexes).toBeUndefined();
expect(userSchema.indexes).toEqual(expectedIndexes);
// Update
await new DefinedSchemas(schemas, server.config).execute();
userSchema = await new Parse.Schema('_User').get();
testSchema = await new Parse.Schema('Test').get();
cleanUpIndexes(userSchema);
cleanUpIndexes(testSchema);
expect(testSchema.indexes).toBeUndefined();
expect(userSchema.indexes).toEqual(expectedIndexes);
});
it('should detect protected indexes for _User class', () => {
const definedSchema = new DefinedSchemas({}, {});
const protectedUserIndexes = ['_id_', 'case_insensitive_email', 'username_1', 'email_1'];
protectedUserIndexes.forEach(field => {
expect(definedSchema.isProtectedIndex('_User', field)).toEqual(true);
});
expect(definedSchema.isProtectedIndex('_User', 'test')).toEqual(false);
});
it('should detect protected indexes for _Role class', () => {
const definedSchema = new DefinedSchemas({}, {});
expect(definedSchema.isProtectedIndex('_Role', 'name_1')).toEqual(true);
expect(definedSchema.isProtectedIndex('_Role', 'test')).toEqual(false);
});
it('should detect protected indexes for _Idempotency class', () => {
const definedSchema = new DefinedSchemas({}, {});
expect(definedSchema.isProtectedIndex('_Idempotency', 'reqId_1')).toEqual(true);
expect(definedSchema.isProtectedIndex('_Idempotency', 'test')).toEqual(false);
});
it('should not detect protected indexes on user defined class', () => {
const definedSchema = new DefinedSchemas({}, {});
const protectedIndexes = [
'case_insensitive_email',
'username_1',
'email_1',
'reqId_1',
'name_1',
];
protectedIndexes.forEach(field => {
expect(definedSchema.isProtectedIndex('ExampleClass', field)).toEqual(false);
});
expect(definedSchema.isProtectedIndex('ExampleClass', '_id_')).toEqual(true);
});
});
describe('ClassLevelPermissions', () => {
it('should use default CLP', async () => {
const server = await reconfigureServer();
const schemas = { definitions: [{ className: 'Test' }] };
await new DefinedSchemas(schemas, server.config).execute();
const expectedTestCLP = {
find: {},
count: {},
get: {},
create: {},
update: {},
delete: {},
addField: {},
protectedFields: {},
};
let testSchema = await new Parse.Schema('Test').get();
expect(testSchema.classLevelPermissions).toEqual(expectedTestCLP);
await new DefinedSchemas(schemas, server.config).execute();
testSchema = await new Parse.Schema('Test').get();
expect(testSchema.classLevelPermissions).toEqual(expectedTestCLP);
});
it('should save CLP', async () => {
const server = await reconfigureServer();
const expectedTestCLP = {
find: {},
count: { requiresAuthentication: true },
get: { 'role:Admin': true },
create: { 'role:ARole': true, requiresAuthentication: true },
update: { requiresAuthentication: true },
delete: { requiresAuthentication: true },
addField: {},
protectedFields: { '*': ['aField'], 'role:Admin': ['anotherField'] },
};
const schemas = {
definitions: [
{
className: 'Test',
fields: { aField: { type: 'String' }, anotherField: { type: 'Object' } },
classLevelPermissions: expectedTestCLP,
},
],
};
await new DefinedSchemas(schemas, server.config).execute();
let testSchema = await new Parse.Schema('Test').get();
expect(testSchema.classLevelPermissions).toEqual(expectedTestCLP);
expectedTestCLP.update = {};
expectedTestCLP.create = { requiresAuthentication: true };
await new DefinedSchemas(schemas, server.config).execute();
testSchema = await new Parse.Schema('Test').get();
expect(testSchema.classLevelPermissions).toEqual(expectedTestCLP);
});
it('should force addField to empty', async () => {
const server = await reconfigureServer();
const schemas = {
definitions: [{ className: 'Test', classLevelPermissions: { addField: { '*': true } } }],
};
await new DefinedSchemas(schemas, server.config).execute();
const expectedTestCLP = {
find: {},
count: {},
get: {},
create: {},
update: {},
delete: {},
addField: {},
protectedFields: {},
};
let testSchema = await new Parse.Schema('Test').get();
expect(testSchema.classLevelPermissions).toEqual(expectedTestCLP);
await new DefinedSchemas(schemas, server.config).execute();
testSchema = await new Parse.Schema('Test').get();
expect(testSchema.classLevelPermissions).toEqual(expectedTestCLP);
});
});
it('should not delete classes automatically', async () => {
await reconfigureServer({
schema: { definitions: [{ className: '_User' }, { className: 'Test' }] },
});
await reconfigureServer({ schema: { definitions: [{ className: '_User' }] } });
const schema = await new Parse.Schema('Test').get();
expect(schema.className).toEqual('Test');
});
it('should disable class PUT/POST endpoint when lockSchemas provided to avoid dual source of truth', async () => {
await reconfigureServer({
schema: {
lockSchemas: true,
definitions: [{ className: '_User' }, { className: 'Test' }],
},
});
const schema = await new Parse.Schema('Test').get();
expect(schema.className).toEqual('Test');
const schemas = await Parse.Schema.all();
// Role could be flaky since all system classes are not ensured
// at start up by the DefinedSchema system
expect(schemas.filter(({ className }) => className !== '_Role').length).toEqual(3);
await expectAsync(new Parse.Schema('TheNewTest').save()).toBeRejectedWithError(
'Cannot perform this operation when schemas options is used.'
);
await expectAsync(new Parse.Schema('_User').update()).toBeRejectedWithError(
'Cannot perform this operation when schemas options is used.'
);
});
it('should only enable delete class endpoint since', async () => {
await reconfigureServer({
schema: { definitions: [{ className: '_User' }, { className: 'Test' }] },
});
await reconfigureServer({ schema: { definitions: [{ className: '_User' }] } });
let schemas = await Parse.Schema.all();
expect(schemas.length).toEqual(4);
await new Parse.Schema('_User').delete();
schemas = await Parse.Schema.all();
expect(schemas.length).toEqual(3);
});
it('should run beforeMigration before execution of DefinedSchemas', async () => {
const config = {
schema: {
definitions: [{ className: '_User' }, { className: 'Test' }],
beforeMigration: async () => {},
},
};
const spy = spyOn(config.schema, 'beforeMigration');
await reconfigureServer(config);
expect(spy).toHaveBeenCalledTimes(1);
});
it('should run afterMigration after execution of DefinedSchemas', async () => {
const config = {
schema: {
definitions: [{ className: '_User' }, { className: 'Test' }],
afterMigration: async () => {},
},
};
const spy = spyOn(config.schema, 'afterMigration');
await reconfigureServer(config);
expect(spy).toHaveBeenCalledTimes(1);
});
it('should use logger in case of error', async () => {
const server = await reconfigureServer({ schema: { definitions: [{ className: '_User' }] } });
const error = new Error('A test error');
const logger = require('../lib/logger').logger;
spyOn(DefinedSchemas.prototype, 'wait').and.resolveTo();
spyOn(logger, 'error').and.callThrough();
spyOn(DefinedSchemas.prototype, 'createDeleteSession').and.callFake(() => {
throw error;
});
await new DefinedSchemas(
{ definitions: [{ className: 'Test', fields: { aField: { type: 'String' } } }] },
server.config
).execute();
expect(logger.error).toHaveBeenCalledWith(`Failed to run migrations: ${error.toString()}`);
});
it_id('a18bf4f2-25c8-4de3-b986-19cb1ab163b8')('should perform migration in parallel without failing', async () => {
const server = await reconfigureServer();
const logger = require('../lib/logger').logger;
spyOn(logger, 'error').and.callThrough();
const migrationOptions = {
definitions: [
{
className: 'Test',
fields: { aField: { type: 'String' } },
indexes: { aField: { aField: 1 } },
classLevelPermissions: {
create: { requiresAuthentication: true },
},
},
],
};
// Simulate parallel deployment
await Promise.all([
new DefinedSchemas(migrationOptions, server.config).execute(),
new DefinedSchemas(migrationOptions, server.config).execute(),
new DefinedSchemas(migrationOptions, server.config).execute(),
new DefinedSchemas(migrationOptions, server.config).execute(),
new DefinedSchemas(migrationOptions, server.config).execute(),
]);
const testSchema = (await Parse.Schema.all()).find(
({ className }) => className === migrationOptions.definitions[0].className
);
expect(testSchema.indexes.aField).toEqual({ aField: 1 });
expect(testSchema.fields.aField).toEqual({ type: 'String' });
expect(testSchema.classLevelPermissions.create).toEqual({ requiresAuthentication: true });
expect(logger.error).toHaveBeenCalledTimes(0);
});
it('should not affect cacheAdapter', async () => {
const server = await reconfigureServer();
const logger = require('../lib/logger').logger;
spyOn(logger, 'error').and.callThrough();
const migrationOptions = {
definitions: [
{
className: 'Test',
fields: { aField: { type: 'String' } },
indexes: { aField: { aField: 1 } },
classLevelPermissions: {
create: { requiresAuthentication: true },
},
},
],
};
const cacheAdapter = {
get: () => Promise.resolve(null),
put: () => {},
del: () => {},
clear: () => {},
connect: jasmine.createSpy('clear'),
};
server.config.cacheAdapter = cacheAdapter;
await new DefinedSchemas(migrationOptions, server.config).execute();
expect(cacheAdapter.connect).not.toHaveBeenCalled();
});
});