'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('Database error')).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); }); }); });