Files
kami-parse-server/spec/index.spec.js

856 lines
28 KiB
JavaScript

'use strict';
const request = require('../lib/request');
const parseServerPackage = require('../package.json');
const MockEmailAdapterWithOptions = require('./support/MockEmailAdapterWithOptions');
const ParseServer = require('../lib/index');
const Config = require('../lib/Config');
const express = require('express');
const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default;
describe('server', () => {
it('requires a master key and app id', done => {
reconfigureServer({ appId: undefined })
.catch(error => {
expect(error).toEqual('You must provide an appId!');
return reconfigureServer({ masterKey: undefined });
})
.catch(error => {
expect(error).toEqual('You must provide a masterKey!');
return reconfigureServer({ serverURL: undefined });
})
.catch(error => {
expect(error).toEqual('You must provide a serverURL!');
done();
});
});
it('show warning if any reserved characters in appId', done => {
spyOn(console, 'warn').and.callFake(() => {});
reconfigureServer({ appId: 'test!-^' }).then(() => {
expect(console.warn).toHaveBeenCalled();
return done();
});
});
it('support http basic authentication with masterkey', done => {
reconfigureServer({ appId: 'test' }).then(() => {
request({
url: 'http://localhost:8378/1/classes/TestObject',
headers: {
Authorization: 'Basic ' + Buffer.from('test:' + 'test').toString('base64'),
},
}).then(response => {
expect(response.status).toEqual(200);
done();
});
});
});
it('support http basic authentication with javascriptKey', done => {
reconfigureServer({ appId: 'test' }).then(() => {
request({
url: 'http://localhost:8378/1/classes/TestObject',
headers: {
Authorization: 'Basic ' + Buffer.from('test:javascript-key=' + 'test').toString('base64'),
},
}).then(response => {
expect(response.status).toEqual(200);
done();
});
});
});
it('fails if database is unreachable', async () => {
spyOn(console, 'error').and.callFake(() => {});
const server = new ParseServer.default({
...defaultConfiguration,
databaseAdapter: new MongoStorageAdapter({
uri: 'mongodb://fake:fake@localhost:43605/drew3',
mongoOptions: {
serverSelectionTimeoutMS: 2000,
},
}),
});
const error = await server.start().catch(e => e);
expect(`${error}`.includes('MongoServerSelectionError')).toBeTrue();
await reconfigureServer();
});
describe('mail adapter', () => {
it('can load email adapter via object', done => {
reconfigureServer({
appName: 'unused',
verifyUserEmails: true,
emailAdapter: MockEmailAdapterWithOptions({
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
}),
publicServerURL: 'http://localhost:8378/1',
}).then(done, fail);
});
it('can load email adapter via class', done => {
reconfigureServer({
appName: 'unused',
verifyUserEmails: true,
emailAdapter: {
class: MockEmailAdapterWithOptions,
options: {
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
},
},
publicServerURL: 'http://localhost:8378/1',
}).then(done, fail);
});
it('can load email adapter via module name', async () => {
const options = {
appName: 'unused',
verifyUserEmails: true,
emailAdapter: {
module: 'mock-mail-adapter',
options: {},
},
publicServerURL: 'http://localhost:8378/1',
};
await reconfigureServer(options);
const config = Config.get('test');
const mailAdapter = config.userController.adapter;
expect(mailAdapter.sendMail).toBeDefined();
});
it('can load email adapter via only module name', async () => {
const options = {
appName: 'unused',
verifyUserEmails: true,
emailAdapter: 'mock-mail-adapter',
publicServerURL: 'http://localhost:8378/1',
};
await reconfigureServer(options);
const config = Config.get('test');
const mailAdapter = config.userController.adapter;
expect(mailAdapter.sendMail).toBeDefined();
});
it('throws if you initialize email adapter incorrectly', async () => {
const options = {
appName: 'unused',
verifyUserEmails: true,
emailAdapter: {
module: 'mock-mail-adapter',
options: { throw: true },
},
publicServerURL: 'http://localhost:8378/1',
};
await expectAsync(reconfigureServer(options)).toBeRejected('MockMailAdapterConstructor');
});
});
it('can report the server version', async done => {
await reconfigureServer();
request({
url: 'http://localhost:8378/1/serverInfo',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
}).then(response => {
const body = response.data;
expect(body.parseServerVersion).toEqual(parseServerPackage.version);
done();
});
});
it('can properly sets the push support', async done => {
await reconfigureServer();
// default config passes push options
const config = Config.get('test');
expect(config.hasPushSupport).toEqual(true);
expect(config.hasPushScheduledSupport).toEqual(false);
request({
url: 'http://localhost:8378/1/serverInfo',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
json: true,
}).then(response => {
const body = response.data;
expect(body.features.push.immediatePush).toEqual(true);
expect(body.features.push.scheduledPush).toEqual(false);
done();
});
});
it('can properly sets the push support when not configured', done => {
reconfigureServer({
push: undefined, // force no config
})
.then(() => {
const config = Config.get('test');
expect(config.hasPushSupport).toEqual(false);
expect(config.hasPushScheduledSupport).toEqual(false);
request({
url: 'http://localhost:8378/1/serverInfo',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
json: true,
}).then(response => {
const body = response.data;
expect(body.features.push.immediatePush).toEqual(false);
expect(body.features.push.scheduledPush).toEqual(false);
done();
});
})
.catch(done.fail);
});
it('can properly sets the push support ', done => {
reconfigureServer({
push: {
adapter: {
send() {},
getValidPushTypes() {},
},
},
})
.then(() => {
const config = Config.get('test');
expect(config.hasPushSupport).toEqual(true);
expect(config.hasPushScheduledSupport).toEqual(false);
request({
url: 'http://localhost:8378/1/serverInfo',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
json: true,
}).then(response => {
const body = response.data;
expect(body.features.push.immediatePush).toEqual(true);
expect(body.features.push.scheduledPush).toEqual(false);
done();
});
})
.catch(done.fail);
});
it('can properly sets the push schedule support', done => {
reconfigureServer({
push: {
adapter: {
send() {},
getValidPushTypes() {},
},
},
scheduledPush: true,
})
.then(() => {
const config = Config.get('test');
expect(config.hasPushSupport).toEqual(true);
expect(config.hasPushScheduledSupport).toEqual(true);
request({
url: 'http://localhost:8378/1/serverInfo',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
json: true,
}).then(response => {
const body = response.data;
expect(body.features.push.immediatePush).toEqual(true);
expect(body.features.push.scheduledPush).toEqual(true);
done();
});
})
.catch(done.fail);
});
it('can respond 200 on path health', done => {
request({
url: 'http://localhost:8378/1/health',
}).then(response => {
expect(response.status).toBe(200);
done();
});
});
it('can create a parse-server v1', async () => {
await reconfigureServer({ appId: 'aTestApp' });
const parseServer = new ParseServer.default(
Object.assign({}, defaultConfiguration, {
appId: 'aTestApp',
masterKey: 'aTestMasterKey',
serverURL: 'http://localhost:12666/parse',
})
);
await parseServer.start();
expect(Parse.applicationId).toEqual('aTestApp');
const app = express();
app.use('/parse', parseServer.app);
const server = app.listen(12666);
const obj = new Parse.Object('AnObject');
await obj.save();
const query = await new Parse.Query('AnObject').first();
expect(obj.id).toEqual(query.id);
await new Promise(resolve => server.close(resolve));
});
it('can create a parse-server v2', async () => {
await reconfigureServer({ appId: 'anOtherTestApp' });
const parseServer = ParseServer.ParseServer(
Object.assign({}, defaultConfiguration, {
appId: 'anOtherTestApp',
masterKey: 'anOtherTestMasterKey',
serverURL: 'http://localhost:12667/parse',
})
);
expect(Parse.applicationId).toEqual('anOtherTestApp');
await parseServer.start();
const app = express();
app.use('/parse', parseServer.app);
const server = app.listen(12667);
const obj = new Parse.Object('AnObject');
await obj.save();
const q = await new Parse.Query('AnObject').first();
expect(obj.id).toEqual(q.id);
await new Promise(resolve => server.close(resolve));
});
it('has createLiveQueryServer', done => {
// original implementation through the factory
expect(typeof ParseServer.ParseServer.createLiveQueryServer).toEqual('function');
// For import calls
expect(typeof ParseServer.default.createLiveQueryServer).toEqual('function');
done();
});
it('exposes correct adapters', done => {
expect(ParseServer.S3Adapter).toThrow(
'S3Adapter is not provided by parse-server anymore; please install @parse/s3-files-adapter'
);
expect(ParseServer.GCSAdapter).toThrow(
'GCSAdapter is not provided by parse-server anymore; please install @parse/gcs-files-adapter'
);
expect(ParseServer.FileSystemAdapter).toThrow();
expect(ParseServer.InMemoryCacheAdapter).toThrow();
expect(ParseServer.NullCacheAdapter).toThrow();
done();
});
it('properly gives publicServerURL when set', done => {
reconfigureServer({ publicServerURL: 'https://myserver.com/1' }).then(() => {
const config = Config.get('test', 'http://localhost:8378/1');
expect(config.mount).toEqual('https://myserver.com/1');
done();
});
});
it('properly removes trailing slash in mount', done => {
reconfigureServer({}).then(() => {
const config = Config.get('test', 'http://localhost:8378/1/');
expect(config.mount).toEqual('http://localhost:8378/1');
done();
});
});
it('should throw when getting invalid mount', done => {
reconfigureServer({ publicServerURL: 'blabla:/some' }).catch(error => {
expect(error).toEqual('The option publicServerURL must be a valid URL starting with http:// or https://.');
done();
});
});
it('should throw when extendSessionOnUse is invalid', async () => {
await expectAsync(
reconfigureServer({
extendSessionOnUse: 'yolo',
})
).toBeRejectedWith('extendSessionOnUse must be a boolean value');
});
it('should throw when revokeSessionOnPasswordReset is invalid', async () => {
await expectAsync(
reconfigureServer({
revokeSessionOnPasswordReset: 'yolo',
})
).toBeRejectedWith('revokeSessionOnPasswordReset must be a boolean value');
});
it('fails if the session length is not a number', done => {
reconfigureServer({ sessionLength: 'test' })
.then(done.fail)
.catch(error => {
expect(error).toEqual('Session length must be a valid number.');
done();
});
});
it('fails if the session length is less than or equal to 0', done => {
reconfigureServer({ sessionLength: '-33' })
.then(done.fail)
.catch(error => {
expect(error).toEqual('Session length must be a value greater than 0.');
return reconfigureServer({ sessionLength: '0' });
})
.catch(error => {
expect(error).toEqual('Session length must be a value greater than 0.');
done();
});
});
it('ignores the session length when expireInactiveSessions set to false', done => {
reconfigureServer({
sessionLength: '-33',
expireInactiveSessions: false,
})
.then(() =>
reconfigureServer({
sessionLength: '0',
expireInactiveSessions: false,
})
)
.then(done);
});
it('fails if default limit is negative', async () => {
await expectAsync(reconfigureServer({ defaultLimit: -1 })).toBeRejectedWith(
'Default limit must be a value greater than 0.'
);
});
it('fails if default limit is wrong type', async () => {
for (const value of ['invalid', {}, [], true]) {
await expectAsync(reconfigureServer({ defaultLimit: value })).toBeRejectedWith(
'Default limit must be a number.'
);
}
});
it('fails if default limit is zero', async () => {
await expectAsync(reconfigureServer({ defaultLimit: 0 })).toBeRejectedWith(
'Default limit must be a value greater than 0.'
);
});
it('fails if maxLimit is negative', done => {
reconfigureServer({ maxLimit: -100 }).catch(error => {
expect(error).toEqual('Max limit must be a value greater than 0.');
done();
});
});
it('fails if you try to set revokeSessionOnPasswordReset to non-boolean', done => {
reconfigureServer({ revokeSessionOnPasswordReset: 'non-bool' }).catch(done);
});
it('fails if you provides invalid ip in masterKeyIps', done => {
reconfigureServer({ masterKeyIps: ['invalidIp', '1.2.3.4'] }).catch(error => {
expect(error).toEqual(
'The Parse Server option "masterKeyIps" contains an invalid IP address "invalidIp".'
);
done();
});
});
it('should succeed if you provide valid ip in masterKeyIps', done => {
reconfigureServer({
masterKeyIps: ['1.2.3.4', '2001:0db8:0000:0042:0000:8a2e:0370:7334'],
}).then(done);
});
it('should set default masterKeyIps for IPv4 and IPv6 localhost', () => {
const definitions = require('../lib/Options/Definitions.js');
expect(definitions.ParseServerOptions.masterKeyIps.default).toEqual(['127.0.0.1', '::1']);
});
it('should load a middleware', done => {
const obj = {
middleware: function (req, res, next) {
next();
},
};
const spy = spyOn(obj, 'middleware').and.callThrough();
reconfigureServer({
middleware: obj.middleware,
})
.then(() => {
const query = new Parse.Query('AnObject');
return query.find();
})
.then(() => {
expect(spy).toHaveBeenCalled();
done();
})
.catch(done.fail);
});
it('should allow direct access', async () => {
const RESTController = Parse.CoreManager.getRESTController();
const spy = spyOn(Parse.CoreManager, 'setRESTController').and.callThrough();
await reconfigureServer({
directAccess: true,
});
expect(spy).toHaveBeenCalledTimes(2);
Parse.CoreManager.setRESTController(RESTController);
});
it('should load a middleware from string', done => {
reconfigureServer({
middleware: 'spec/support/CustomMiddleware',
})
.then(() => {
return request({ url: 'http://localhost:8378/1' }).then(fail, res => {
// Just check that the middleware set the header
expect(res.headers['x-yolo']).toBe('1');
done();
});
})
.catch(done.fail);
});
it('can call start', async () => {
await reconfigureServer({ appId: 'aTestApp' });
const config = {
...defaultConfiguration,
appId: 'aTestApp',
masterKey: 'aTestMasterKey',
serverURL: 'http://localhost:12701/parse',
};
const parseServer = new ParseServer.ParseServer(config);
await parseServer.start();
expect(Parse.applicationId).toEqual('aTestApp');
expect(Parse.serverURL).toEqual('http://localhost:12701/parse');
const app = express();
app.use('/parse', parseServer.app);
const server = app.listen(12701);
const testObject = new Parse.Object('TestObject');
await expectAsync(testObject.save()).toBeResolved();
await new Promise(resolve => server.close(resolve));
});
it('start is required to mount', async () => {
await reconfigureServer({ appId: 'aTestApp' });
const config = {
...defaultConfiguration,
appId: 'aTestApp',
masterKey: 'aTestMasterKey',
serverURL: 'http://localhost:12701/parse',
};
const parseServer = new ParseServer.ParseServer(config);
expect(Parse.applicationId).toEqual('aTestApp');
expect(Parse.serverURL).toEqual('http://localhost:12701/parse');
const app = express();
app.use('/parse', parseServer.app);
const server = app.listen(12701);
const response = await request({
headers: {
'X-Parse-Application-Id': 'aTestApp',
},
method: 'POST',
url: 'http://localhost:12701/parse/classes/TestObject',
}).catch(e => new Parse.Error(e.data.code, e.data.error));
expect(response).toEqual(
new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Invalid server state: initialized')
);
const health = await request({
url: 'http://localhost:12701/parse/health',
}).catch(e => e);
spyOn(console, 'warn').and.callFake(() => {});
const verify = await ParseServer.default.verifyServerUrl();
expect(verify).not.toBeTrue();
expect(console.warn).toHaveBeenCalledWith(
`\nWARNING, Unable to connect to 'http://localhost:12701/parse'. Cloud code and push notifications may be unavailable!\n`
);
expect(health.data.status).toBe('initialized');
expect(health.status).toBe(503);
await new Promise(resolve => server.close(resolve));
});
it('can get starting state', async () => {
await reconfigureServer({ appId: 'test2' });
const parseServer = new ParseServer.ParseServer({
...defaultConfiguration,
appId: 'test2',
masterKey: 'abc',
serverURL: 'http://localhost:12668/parse',
async cloud() {
await new Promise(resolve => setTimeout(resolve, 2000));
},
});
const express = require('express');
const app = express();
app.use('/parse', parseServer.app);
const server = app.listen(12668);
const startingPromise = parseServer.start();
const health = await request({
url: 'http://localhost:12668/parse/health',
}).catch(e => e);
expect(health.data.status).toBe('starting');
expect(health.status).toBe(503);
expect(health.headers['retry-after']).toBe('1');
const response = await ParseServer.default.verifyServerUrl();
expect(response).toBeTrue();
await startingPromise;
await new Promise(resolve => server.close(resolve));
});
it('should load masterKey', async () => {
await reconfigureServer({
masterKey: () => 'testMasterKey',
masterKeyTtl: 1000, // TTL is set
});
await new Parse.Object('TestObject').save();
const config = Config.get(Parse.applicationId);
expect(config.masterKeyCache.masterKey).toEqual('testMasterKey');
expect(config.masterKeyCache.expiresAt.getTime()).toBeGreaterThan(Date.now());
});
it('should not reload if ttl is not set', async () => {
const masterKeySpy = jasmine.createSpy().and.returnValue(Promise.resolve('initialMasterKey'));
await reconfigureServer({
masterKey: masterKeySpy,
masterKeyTtl: null, // No TTL set
});
await new Parse.Object('TestObject').save();
const config = Config.get(Parse.applicationId);
const firstMasterKey = config.masterKeyCache.masterKey;
// Simulate calling the method again
await config.loadMasterKey();
const secondMasterKey = config.masterKeyCache.masterKey;
expect(firstMasterKey).toEqual('initialMasterKey');
expect(secondMasterKey).toEqual('initialMasterKey');
expect(masterKeySpy).toHaveBeenCalledTimes(1); // Should only be called once
expect(config.masterKeyCache.expiresAt).toBeNull(); // TTL is not set, so expiresAt should remain null
});
it('should reload masterKey if ttl is set and expired', async () => {
const masterKeySpy = jasmine.createSpy()
.and.returnValues(Promise.resolve('firstMasterKey'), Promise.resolve('secondMasterKey'));
await reconfigureServer({
masterKey: masterKeySpy,
masterKeyTtl: 1 / 1000, // TTL is set to 1ms
});
await new Parse.Object('TestObject').save();
await new Promise(resolve => setTimeout(resolve, 10));
await new Parse.Object('TestObject').save();
const config = Config.get(Parse.applicationId);
expect(masterKeySpy).toHaveBeenCalledTimes(2);
expect(config.masterKeyCache.masterKey).toEqual('secondMasterKey');
});
it('should not fail when Google signin is introduced without the optional clientId', done => {
const jwt = require('jsonwebtoken');
const authUtils = require('../lib/Adapters/Auth/utils');
reconfigureServer({
auth: { google: {} },
})
.then(() => {
const fakeClaim = {
iss: 'https://accounts.google.com',
aud: 'secret',
exp: Date.now(),
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
const user = new Parse.User();
user
.linkWith('google', {
authData: { id: 'the_user_id', id_token: 'the_token' },
})
.then(done);
})
.catch(done.fail);
});
describe('publicServerURL', () => {
it('should load publicServerURL', async () => {
await reconfigureServer({
publicServerURL: () => 'https://example.com/1',
});
await new Parse.Object('TestObject').save();
const config = Config.get(Parse.applicationId);
expect(config.publicServerURL).toEqual('https://example.com/1');
});
it('should load publicServerURL from Promise', async () => {
await reconfigureServer({
publicServerURL: () => Promise.resolve('https://example.com/1'),
});
await new Parse.Object('TestObject').save();
const config = Config.get(Parse.applicationId);
expect(config.publicServerURL).toEqual('https://example.com/1');
});
it('should handle publicServerURL function throwing error', async () => {
const errorMessage = 'Failed to get public server URL';
await reconfigureServer({
publicServerURL: () => {
throw new Error(errorMessage);
},
});
// The error should occur when trying to save an object (which triggers loadKeys in middleware)
await expectAsync(
new Parse.Object('TestObject').save()
).toBeRejected();
});
it('should handle publicServerURL Promise rejection', async () => {
const errorMessage = 'Async fetch of public server URL failed';
await reconfigureServer({
publicServerURL: () => Promise.reject(new Error(errorMessage)),
});
// The error should occur when trying to save an object (which triggers loadKeys in middleware)
await expectAsync(
new Parse.Object('TestObject').save()
).toBeRejected();
});
it('executes publicServerURL function on every config access', async () => {
let counter = 0;
await reconfigureServer({
publicServerURL: () => {
counter++;
return `https://example.com/${counter}`;
},
});
// First request - should call the function
await new Parse.Object('TestObject').save();
const config1 = Config.get(Parse.applicationId);
expect(config1.publicServerURL).toEqual('https://example.com/1');
expect(counter).toEqual(1);
// Second request - should call the function again
await new Parse.Object('TestObject').save();
const config2 = Config.get(Parse.applicationId);
expect(config2.publicServerURL).toEqual('https://example.com/2');
expect(counter).toEqual(2);
// Third request - should call the function again
await new Parse.Object('TestObject').save();
const config3 = Config.get(Parse.applicationId);
expect(config3.publicServerURL).toEqual('https://example.com/3');
expect(counter).toEqual(3);
});
it('executes publicServerURL function on every password reset email', async () => {
let counter = 0;
const emailCalls = [];
const emailAdapter = MockEmailAdapterWithOptions({
sendPasswordResetEmail: ({ link }) => {
emailCalls.push(link);
return Promise.resolve();
},
});
await reconfigureServer({
appName: 'test-app',
publicServerURL: () => {
counter++;
return `https://example.com/${counter}`;
},
emailAdapter,
});
// Create a user
const user = new Parse.User();
user.setUsername('user');
user.setPassword('pass');
user.setEmail('user@example.com');
await user.signUp();
// Should use first publicServerURL
const counterBefore1 = counter;
await Parse.User.requestPasswordReset('user@example.com');
await jasmine.timeout();
expect(emailCalls.length).toEqual(1);
expect(emailCalls[0]).toContain(`https://example.com/${counterBefore1 + 1}`);
expect(counter).toBeGreaterThanOrEqual(2);
// Should use updated publicServerURL
const counterBefore2 = counter;
await Parse.User.requestPasswordReset('user@example.com');
await jasmine.timeout();
expect(emailCalls.length).toEqual(2);
expect(emailCalls[1]).toContain(`https://example.com/${counterBefore2 + 1}`);
expect(counterBefore2).toBeGreaterThan(counterBefore1);
});
it('executes publicServerURL function on every verification email', async () => {
let counter = 0;
const emailCalls = [];
const emailAdapter = MockEmailAdapterWithOptions({
sendVerificationEmail: ({ link }) => {
emailCalls.push(link);
return Promise.resolve();
},
});
await reconfigureServer({
appName: 'test-app',
verifyUserEmails: true,
publicServerURL: () => {
counter++;
return `https://example.com/${counter}`;
},
emailAdapter,
});
// Should trigger verification email with first publicServerURL
const counterBefore1 = counter;
const user1 = new Parse.User();
user1.setUsername('user1');
user1.setPassword('pass1');
user1.setEmail('user1@example.com');
await user1.signUp();
await jasmine.timeout();
expect(emailCalls.length).toEqual(1);
expect(emailCalls[0]).toContain(`https://example.com/${counterBefore1 + 1}`);
// Should trigger verification email with updated publicServerURL
const counterBefore2 = counter;
const user2 = new Parse.User();
user2.setUsername('user2');
user2.setPassword('pass2');
user2.setEmail('user2@example.com');
await user2.signUp();
await jasmine.timeout();
expect(emailCalls.length).toEqual(2);
expect(emailCalls[1]).toContain(`https://example.com/${counterBefore2 + 1}`);
expect(counterBefore2).toBeGreaterThan(counterBefore1);
});
});
});