diff --git a/spec/EnableExpressErrorHandler.spec.js b/spec/EnableExpressErrorHandler.spec.js index 26483ec6a1..64c250628b 100644 --- a/spec/EnableExpressErrorHandler.spec.js +++ b/spec/EnableExpressErrorHandler.spec.js @@ -3,11 +3,9 @@ const request = require('../lib/request'); describe('Enable express error handler', () => { it('should call the default handler in case of error, like updating a non existing object', async done => { spyOn(console, 'error'); - const parseServer = await reconfigureServer( - Object.assign({}, defaultConfiguration, { - enableExpressErrorHandler: true, - }) - ); + const parseServer = await reconfigureServer({ + enableExpressErrorHandler: true, + }); parseServer.app.use(function (err, req, res, next) { expect(err.message).toBe('Object not found.'); next(err); diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index a178a1b863..6edfa79109 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -33,9 +33,7 @@ describe('miscellaneous', () => { expect(results.length).toEqual(1); expect(results[0]['foo']).toEqual('bar'); }); -}); -describe('miscellaneous', function () { it('create a GameScore object', function (done) { const obj = new Parse.Object('GameScore'); obj.set('score', 1337); diff --git a/spec/ParseConfigKey.spec.js b/spec/ParseConfigKey.spec.js index fe7556123b..a171032087 100644 --- a/spec/ParseConfigKey.spec.js +++ b/spec/ParseConfigKey.spec.js @@ -12,7 +12,6 @@ describe('Config Keys', () => { it('recognizes invalid keys in root', async () => { await expectAsync(reconfigureServer({ - ...defaultConfiguration, invalidKey: 1, })).toBeResolved(); const error = loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], ''); @@ -21,7 +20,6 @@ describe('Config Keys', () => { it('recognizes invalid keys in pages.customUrls', async () => { await expectAsync(reconfigureServer({ - ...defaultConfiguration, pages: { customUrls: { invalidKey: 1, @@ -37,7 +35,6 @@ describe('Config Keys', () => { it('recognizes invalid keys in liveQueryServerOptions', async () => { await expectAsync(reconfigureServer({ - ...defaultConfiguration, liveQueryServerOptions: { invalidKey: 1, MasterKey: 1, @@ -50,7 +47,6 @@ describe('Config Keys', () => { it('recognizes invalid keys in rateLimit', async () => { await expectAsync(reconfigureServer({ - ...defaultConfiguration, rateLimit: [ { invalidKey: 1 }, { RequestPath: 1 }, @@ -64,7 +60,7 @@ describe('Config Keys', () => { expect(error).toMatch('rateLimit\\[2\\]\\.RequestTimeWindow'); }); - it('recognizes valid keys in default configuration', async () => { + it_only_db('mongo')('recognizes valid keys in default configuration', async () => { await expectAsync(reconfigureServer({ ...defaultConfiguration, })).toBeResolved(); diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index e1353d8db2..414310d05e 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -431,17 +431,32 @@ describe('ParseGraphQLServer', () => { objects.push(object1, object2, object3, object4); } - beforeEach(async () => { + async function createGQLFromParseServer(_parseServer) { + if (parseLiveQueryServer) { + await parseLiveQueryServer.server.close(); + } + if (httpServer) { + await httpServer.close(); + } const expressApp = express(); httpServer = http.createServer(expressApp); - expressApp.use('/parse', parseServer.app); + expressApp.use('/parse', _parseServer.app); parseLiveQueryServer = await ParseServer.createLiveQueryServer(httpServer, { port: 1338, }); + parseGraphQLServer = new ParseGraphQLServer(_parseServer, { + graphQLPath: '/graphql', + playgroundPath: '/playground', + subscriptionsPath: '/subscriptions', + }); parseGraphQLServer.applyGraphQL(expressApp); parseGraphQLServer.applyPlayground(expressApp); parseGraphQLServer.createSubscriptions(httpServer); await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve)); + } + + beforeEach(async () => { + await createGQLFromParseServer(parseServer); const subscriptionClient = new SubscriptionClient( 'ws://localhost:13377/subscriptions', @@ -753,10 +768,6 @@ describe('ParseGraphQLServer', () => { } }); - afterAll(async () => { - await resetGraphQLCache(); - }); - it('should have Node interface', async () => { const schemaTypes = ( await apolloClient.query({ @@ -2821,7 +2832,8 @@ describe('ParseGraphQLServer', () => { } }); it('Id inputs should work either with global id or object id with objectId higher than 19', async () => { - await reconfigureServer({ objectIdSize: 20 }); + const parseServer = await reconfigureServer({ objectIdSize: 20 }); + await createGQLFromParseServer(parseServer); const obj = new Parse.Object('SomeClass'); await obj.save({ name: 'aname', type: 'robot' }); const result = await apolloClient.query({ @@ -5328,7 +5340,7 @@ describe('ParseGraphQLServer', () => { parseServer = await global.reconfigureServer({ maxLimit: 10, }); - + await createGQLFromParseServer(parseServer); const promises = []; for (let i = 0; i < 100; i++) { const obj = new Parse.Object('SomeClass'); @@ -6841,7 +6853,7 @@ describe('ParseGraphQLServer', () => { parseServer = await global.reconfigureServer({ publicServerURL: 'http://localhost:13377/parse', }); - + await createGQLFromParseServer(parseServer); const body = new FormData(); body.append( 'operations', @@ -7049,6 +7061,7 @@ describe('ParseGraphQLServer', () => { challengeAdapter, }, }); + await createGQLFromParseServer(parseServer); const clientMutationId = uuidv4(); const result = await apolloClient.mutate({ @@ -7095,6 +7108,7 @@ describe('ParseGraphQLServer', () => { challengeAdapter, }, }); + await createGQLFromParseServer(parseServer); const clientMutationId = uuidv4(); const userSchema = new Parse.Schema('_User'); userSchema.addString('someField'); @@ -7169,7 +7183,7 @@ describe('ParseGraphQLServer', () => { }, }, }); - + await createGQLFromParseServer(parseServer); userSchema.addString('someField'); userSchema.addPointer('aPointer', '_User'); await userSchema.update(); @@ -7239,7 +7253,7 @@ describe('ParseGraphQLServer', () => { challengeAdapter, }, }); - + await createGQLFromParseServer(parseServer); const user = new Parse.User(); await user.save({ username: 'username', password: 'password' }); @@ -7310,6 +7324,7 @@ describe('ParseGraphQLServer', () => { challengeAdapter, }, }); + await createGQLFromParseServer(parseServer); const clientMutationId = uuidv4(); const user = new Parse.User(); user.setUsername('user1'); @@ -7441,6 +7456,7 @@ describe('ParseGraphQLServer', () => { emailAdapter: emailAdapter, publicServerURL: 'http://test.test', }); + await createGQLFromParseServer(parseServer); const user = new Parse.User(); user.setUsername('user1'); user.setPassword('user1'); @@ -7488,6 +7504,7 @@ describe('ParseGraphQLServer', () => { }, }, }); + await createGQLFromParseServer(parseServer); const user = new Parse.User(); user.setUsername('user1'); user.setPassword('user1'); @@ -7550,6 +7567,7 @@ describe('ParseGraphQLServer', () => { emailAdapter: emailAdapter, publicServerURL: 'http://test.test', }); + await createGQLFromParseServer(parseServer); const user = new Parse.User(); user.setUsername('user1'); user.setPassword('user1'); @@ -9306,7 +9324,7 @@ describe('ParseGraphQLServer', () => { parseServer = await global.reconfigureServer({ publicServerURL: 'http://localhost:13377/parse', }); - + await createGQLFromParseServer(parseServer); const body = new FormData(); body.append( 'operations', @@ -9339,7 +9357,6 @@ describe('ParseGraphQLServer', () => { headers, body, }); - expect(res.status).toEqual(200); const result = JSON.parse(await res.text()); @@ -9553,6 +9570,7 @@ describe('ParseGraphQLServer', () => { parseServer = await global.reconfigureServer({ publicServerURL: 'http://localhost:13377/parse', }); + await createGQLFromParseServer(parseServer); const schemaController = await parseServer.config.databaseController.loadSchema(); await schemaController.addClassIfNotExists('SomeClassWithRequiredFile', { someField: { type: 'File', required: true }, @@ -9617,6 +9635,7 @@ describe('ParseGraphQLServer', () => { parseServer = await global.reconfigureServer({ publicServerURL: 'http://localhost:13377/parse', }); + await createGQLFromParseServer(parseServer); const schema = new Parse.Schema('SomeClass'); schema.addFile('someFileField'); schema.addPointer('somePointerField', 'SomeClass'); @@ -9725,7 +9744,7 @@ describe('ParseGraphQLServer', () => { parseServer = await global.reconfigureServer({ publicServerURL: 'http://localhost:13377/parse', }); - + await createGQLFromParseServer(parseServer); const body = new FormData(); body.append( 'operations', diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index 6294c609a1..807a92641f 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -1,10 +1,12 @@ 'use strict'; +const http = require('http'); const Auth = require('../lib/Auth'); const UserController = require('../lib/Controllers/UserController').UserController; const Config = require('../lib/Config'); const ParseServer = require('../lib/index').ParseServer; const triggers = require('../lib/triggers'); -const { resolvingPromise, sleep } = require('../lib/TestUtils'); +const { resolvingPromise, sleep, getConnectionsCount } = require('../lib/TestUtils'); +const request = require('../lib/request'); const validatorFail = () => { throw 'you are not authorized'; }; @@ -1181,6 +1183,78 @@ describe('ParseLiveQuery', function () { await new Promise(resolve => server.server.close(resolve)); }); + it_id('45655b74-716f-4fa1-a058-67eb21f3c3db')(it)('does shutdown separate liveQuery server', async () => { + await reconfigureServer({ appId: 'test_app_id' }); + let close = false; + const config = { + appId: 'hello_test', + masterKey: 'world', + port: 1345, + mountPath: '/1', + serverURL: 'http://localhost:1345/1', + liveQuery: { + classNames: ['Yolo'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + liveQueryServerOptions: { + port: 1346, + }, + serverCloseComplete: () => { + close = true; + }, + }; + if (process.env.PARSE_SERVER_TEST_DB === 'postgres') { + config.databaseAdapter = new databaseAdapter.constructor({ + uri: databaseURI, + collectionPrefix: 'test_', + }); + config.filesAdapter = defaultConfiguration.filesAdapter; + } + const parseServer = await ParseServer.startApp(config); + expect(parseServer.liveQueryServer).toBeDefined(); + expect(parseServer.liveQueryServer.server).not.toBe(parseServer.server); + + // Open a connection to the liveQuery server + const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); + client.serverURL = 'ws://localhost:1346/1'; + const query = await new Parse.Query('Yolo').subscribe(); + + // Open a connection to the parse server + const health = await request({ + method: 'GET', + url: `http://localhost:1345/1/health`, + json: true, + headers: { + 'X-Parse-Application-Id': 'hello_test', + 'X-Parse-Master-Key': 'world', + 'Content-Type': 'application/json', + }, + agent: new http.Agent({ keepAlive: true }), + }).then(res => res.data); + expect(health.status).toBe('ok'); + + let parseConnectionCount = await getConnectionsCount(parseServer.server); + let liveQueryConnectionCount = await getConnectionsCount(parseServer.liveQueryServer.server); + + expect(parseConnectionCount > 0).toBe(true); + expect(liveQueryConnectionCount > 0).toBe(true); + await Promise.all([ + parseServer.handleShutdown(), + new Promise(resolve => query.on('close', resolve)), + ]); + expect(close).toBe(true); + await new Promise(resolve => setTimeout(resolve, 100)); + expect(parseServer.liveQueryServer.server.address()).toBeNull(); + expect(parseServer.liveQueryServer.subscriber.isOpen).toBeFalse(); + + parseConnectionCount = await getConnectionsCount(parseServer.server); + liveQueryConnectionCount = await getConnectionsCount(parseServer.liveQueryServer.server); + expect(parseConnectionCount).toBe(0); + expect(liveQueryConnectionCount).toBe(0); + }); + it('prevent afterSave trigger if not exists', async () => { await reconfigureServer({ liveQuery: { diff --git a/spec/ParseQuery.FullTextSearch.spec.js b/spec/ParseQuery.FullTextSearch.spec.js index 11760ec161..d11d1ba86a 100644 --- a/spec/ParseQuery.FullTextSearch.spec.js +++ b/spec/ParseQuery.FullTextSearch.spec.js @@ -3,11 +3,8 @@ const Config = require('../lib/Config'); const Parse = require('parse/node'); const request = require('../lib/request'); -let databaseAdapter; const fullTextHelper = async () => { - const config = Config.get('test'); - databaseAdapter = config.database.adapter; const subjects = [ 'coffee', 'Coffee Shopping', @@ -18,12 +15,6 @@ const fullTextHelper = async () => { 'coffee and cream', 'Cafe con Leche', ]; - await reconfigureServer({ - appId: 'test', - restAPIKey: 'test', - publicServerURL: 'http://localhost:8378/1', - databaseAdapter, - }); await Parse.Object.saveAll( subjects.map(subject => new Parse.Object('TestObject').set({ subject, comment: subject })) ); @@ -101,7 +92,7 @@ describe('Parse.Query Full Text Search testing', () => { body: { where, _method: 'GET' }, headers: { 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test', + 'X-Parse-REST-API-Key': 'rest', 'Content-Type': 'application/json', }, }); @@ -189,7 +180,7 @@ describe_only_db('mongo')('[mongodb] Parse.Query Full Text Search testing', () = url: 'http://localhost:8378/1/schemas/TestObject', headers: { 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test', + 'X-Parse-REST-API-Key': 'rest', 'X-Parse-Master-Key': 'test', 'Content-Type': 'application/json', }, @@ -220,7 +211,7 @@ describe_only_db('mongo')('[mongodb] Parse.Query Full Text Search testing', () = body: { where, _method: 'GET' }, headers: { 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test', + 'X-Parse-REST-API-Key': 'rest', 'Content-Type': 'application/json', }, }); @@ -288,7 +279,7 @@ describe_only_db('postgres')('[postgres] Parse.Query Full Text Search testing', body: { where, _method: 'GET' }, headers: { 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test', + 'X-Parse-REST-API-Key': 'rest', 'Content-Type': 'application/json', }, }); @@ -322,7 +313,7 @@ describe_only_db('postgres')('[postgres] Parse.Query Full Text Search testing', body: { where, _method: 'GET' }, headers: { 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test', + 'X-Parse-REST-API-Key': 'rest', 'Content-Type': 'application/json', }, }); diff --git a/spec/ParseServer.spec.js b/spec/ParseServer.spec.js index b55c6fc548..ec12d6f7fd 100644 --- a/spec/ParseServer.spec.js +++ b/spec/ParseServer.spec.js @@ -1,9 +1,6 @@ 'use strict'; /* Tests for ParseServer.js */ const express = require('express'); -const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default; -const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter') - .default; const ParseServer = require('../lib/ParseServer').default; const path = require('path'); const { spawn } = require('child_process'); @@ -45,49 +42,6 @@ describe('Server Url Checks', () => { ); }); - xit('handleShutdown, close connection', done => { - const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; - const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; - let databaseAdapter; - if (process.env.PARSE_SERVER_TEST_DB === 'postgres') { - databaseAdapter = new PostgresStorageAdapter({ - uri: process.env.PARSE_SERVER_TEST_DATABASE_URI || postgresURI, - collectionPrefix: 'test_', - }); - } else { - databaseAdapter = new MongoStorageAdapter({ - uri: mongoURI, - collectionPrefix: 'test_', - }); - } - let close = false; - const newConfiguration = Object.assign({}, defaultConfiguration, { - databaseAdapter, - serverStartComplete: () => { - let promise = Promise.resolve(); - if (process.env.PARSE_SERVER_TEST_DB !== 'postgres') { - promise = parseServer.config.filesController.adapter._connect(); - } - promise.then(() => { - parseServer.handleShutdown(); - parseServer.server.close(err => { - if (err) { - done.fail('Close Server Error'); - } - reconfigureServer({}).then(() => { - expect(close).toBe(true); - done(); - }); - }); - }); - }, - serverCloseComplete: () => { - close = true; - }, - }); - const parseServer = ParseServer.startApp(newConfiguration); - }); - it('does not have unhandled promise rejection in the case of load error', done => { const parseServerProcess = spawn(path.resolve(__dirname, './support/FailingServer.js')); let stdout; diff --git a/spec/ParseServerRESTController.spec.js b/spec/ParseServerRESTController.spec.js index fbc244ab87..31d1f5aec7 100644 --- a/spec/ParseServerRESTController.spec.js +++ b/spec/ParseServerRESTController.spec.js @@ -282,7 +282,6 @@ describe('ParseServerRESTController', () => { }); it('should generate separate session for each call', async () => { - await reconfigureServer(); const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections await myObject.save({ key: 'stringField' }); await myObject.destroy(); diff --git a/spec/PushController.spec.js b/spec/PushController.spec.js index af29156a16..b914ceac84 100644 --- a/spec/PushController.spec.js +++ b/spec/PushController.spec.js @@ -182,6 +182,9 @@ describe('PushController', () => { return ['ios', 'android']; }, }; + await reconfigureServer({ + push: { adapter: pushAdapter }, + }); const payload = { data: { alert: 'Hello World!', @@ -212,9 +215,6 @@ describe('PushController', () => { const auth = { isMaster: true, }; - await reconfigureServer({ - push: { adapter: pushAdapter }, - }); await Parse.Object.saveAll(installations); const pushStatusId = await sendPush(payload, {}, config, auth); await pushCompleted(pushStatusId); @@ -247,6 +247,9 @@ describe('PushController', () => { return ['ios', 'android']; }, }; + await reconfigureServer({ + push: { adapter: pushAdapter }, + }); const payload = { data: { alert: 'Hello World!', @@ -277,9 +280,6 @@ describe('PushController', () => { const auth = { isMaster: true, }; - await reconfigureServer({ - push: { adapter: pushAdapter }, - }); await Parse.Object.saveAll(installations); const pushStatusId = await sendPush(payload, {}, config, auth); await pushCompleted(pushStatusId); @@ -309,7 +309,9 @@ describe('PushController', () => { return ['ios']; }, }; - + await reconfigureServer({ + push: { adapter: pushAdapter }, + }); const payload = { data: { alert: 'Hello World!', @@ -331,9 +333,6 @@ describe('PushController', () => { const auth = { isMaster: true, }; - await reconfigureServer({ - push: { adapter: pushAdapter }, - }); await Parse.Object.saveAll(installations); const pushStatusId = await sendPush(payload, {}, config, auth); await pushCompleted(pushStatusId); @@ -382,14 +381,13 @@ describe('PushController', () => { return ['ios']; }, }; - + await reconfigureServer({ + push: { adapter: pushAdapter }, + }); const config = Config.get(Parse.applicationId); const auth = { isMaster: true, }; - await reconfigureServer({ - push: { adapter: pushAdapter }, - }); await Parse.Object.saveAll(installations); const objectIds = installations.map(installation => { return installation.id; @@ -445,14 +443,13 @@ describe('PushController', () => { return ['ios']; }, }; - + await reconfigureServer({ + push: { adapter: pushAdapter }, + }); const config = Config.get(Parse.applicationId); const auth = { isMaster: true, }; - await reconfigureServer({ - push: { adapter: pushAdapter }, - }); await Parse.Object.saveAll(installations); const pushStatusId = await sendPush(payload, {}, config, auth); await pushCompleted(pushStatusId); @@ -548,16 +545,15 @@ describe('PushController', () => { return ['ios']; }, }; - - const config = Config.get(Parse.applicationId); - const auth = { - isMaster: true, - }; await installation.save(); await reconfigureServer({ serverURL: 'http://localhost:8378/', // server with borked URL push: { adapter: pushAdapter }, }); + const config = Config.get(Parse.applicationId); + const auth = { + isMaster: true, + }; const pushStatusId = await sendPush(payload, {}, config, auth); // it is enqueued so it can take time await jasmine.timeout(1000); @@ -580,6 +576,9 @@ describe('PushController', () => { return ['ios']; }, }; + await reconfigureServer({ + push: { adapter: pushAdapter }, + }); // $ins is invalid query const where = { channels: { @@ -596,9 +595,6 @@ describe('PushController', () => { isMaster: true, }; const pushController = new PushController(); - await reconfigureServer({ - push: { adapter: pushAdapter }, - }); const config = Config.get(Parse.applicationId); try { await pushController.sendPush(payload, where, config, auth); @@ -631,6 +627,9 @@ describe('PushController', () => { return ['ios']; }, }; + await reconfigureServer({ + push: { adapter: pushAdapter }, + }); const config = Config.get(Parse.applicationId); const auth = { isMaster: true, @@ -641,9 +640,6 @@ describe('PushController', () => { $in: ['device_token_0', 'device_token_1', 'device_token_2'], }, }; - await reconfigureServer({ - push: { adapter: pushAdapter }, - }); const installations = []; while (installations.length != 5) { const installation = new Parse.Object('_Installation'); @@ -678,7 +674,9 @@ describe('PushController', () => { return ['ios']; }, }; - + await reconfigureServer({ + push: { adapter: pushAdapter }, + }); const config = Config.get(Parse.applicationId); const auth = { isMaster: true, @@ -686,9 +684,6 @@ describe('PushController', () => { const where = { deviceType: 'ios', }; - await reconfigureServer({ - push: { adapter: pushAdapter }, - }); const installations = []; while (installations.length != 5) { const installation = new Parse.Object('_Installation'); @@ -762,10 +757,6 @@ describe('PushController', () => { }); it('should not schedule push when not configured', async () => { - const config = Config.get(Parse.applicationId); - const auth = { - isMaster: true, - }; const pushAdapter = { send: function (body, installations) { return successfulTransmissions(body, installations); @@ -774,7 +765,13 @@ describe('PushController', () => { return ['ios']; }, }; - + await reconfigureServer({ + push: { adapter: pushAdapter }, + }); + const config = Config.get(Parse.applicationId); + const auth = { + isMaster: true, + }; const pushController = new PushController(); const payload = { data: { @@ -793,10 +790,6 @@ describe('PushController', () => { installation.set('deviceType', 'ios'); installations.push(installation); } - - await reconfigureServer({ - push: { adapter: pushAdapter }, - }); await Parse.Object.saveAll(installations); await pushController.sendPush(payload, {}, config, auth); await jasmine.timeout(1000); @@ -986,6 +979,10 @@ describe('PushController', () => { return ['ios']; }, }; + spyOn(pushAdapter, 'send').and.callThrough(); + await reconfigureServer({ + push: { adapter: pushAdapter }, + }); const config = Config.get(Parse.applicationId); const auth = { isMaster: true, @@ -1007,10 +1004,6 @@ describe('PushController', () => { installations[1].set('localeIdentifier', 'fr-FR'); installations[2].set('localeIdentifier', 'en-US'); - spyOn(pushAdapter, 'send').and.callThrough(); - await reconfigureServer({ - push: { adapter: pushAdapter }, - }); await Parse.Object.saveAll(installations); const pushStatusId = await sendPush(payload, where, config, auth); await pushCompleted(pushStatusId); @@ -1039,7 +1032,10 @@ describe('PushController', () => { return ['ios']; }, }; - + spyOn(pushAdapter, 'send').and.callThrough(); + await reconfigureServer({ + push: { adapter: pushAdapter }, + }); const config = Config.get(Parse.applicationId); const auth = { isMaster: true, @@ -1060,10 +1056,6 @@ describe('PushController', () => { installation.set('deviceType', 'ios'); installations.push(installation); } - spyOn(pushAdapter, 'send').and.callThrough(); - await reconfigureServer({ - push: { adapter: pushAdapter }, - }); await Parse.Object.saveAll(installations); // Create an audience diff --git a/spec/SchemaPerformance.spec.js b/spec/SchemaPerformance.spec.js index 729401d068..415f71e2e5 100644 --- a/spec/SchemaPerformance.spec.js +++ b/spec/SchemaPerformance.spec.js @@ -5,10 +5,8 @@ describe('Schema Performance', function () { let config; beforeEach(async () => { + await reconfigureServer(); config = Config.get('test'); - config.schemaCache.clear(); - const databaseAdapter = config.database.adapter; - await reconfigureServer({ databaseAdapter }); getAllSpy = spyOn(databaseAdapter, 'getAllClasses').and.callThrough(); }); diff --git a/spec/SecurityCheckGroups.spec.js b/spec/SecurityCheckGroups.spec.js index 7faca01898..21409a78c1 100644 --- a/spec/SecurityCheckGroups.spec.js +++ b/spec/SecurityCheckGroups.spec.js @@ -67,18 +67,22 @@ describe('Security Check Groups', () => { it('checks succeed correctly', async () => { const config = Config.get(Parse.applicationId); + const uri = config.database.adapter._uri; config.database.adapter._uri = 'protocol://user:aMoreSecur3Passwor7!@example.com'; const group = new CheckGroupDatabase(); await group.run(); expect(group.checks()[0].checkState()).toBe(CheckState.success); + config.database.adapter._uri = uri; }); it('checks fail correctly', async () => { const config = Config.get(Parse.applicationId); + const uri = config.database.adapter._uri; config.database.adapter._uri = 'protocol://user:insecure@example.com'; const group = new CheckGroupDatabase(); await group.run(); expect(group.checks()[0].checkState()).toBe(CheckState.fail); + config.database.adapter._uri = uri; }); }); }); diff --git a/spec/batch.spec.js b/spec/batch.spec.js index 8c9ef27a6b..9fc9ccdb48 100644 --- a/spec/batch.spec.js +++ b/spec/batch.spec.js @@ -366,7 +366,6 @@ describe('batch', () => { }); it('should generate separate session for each call', async () => { - await reconfigureServer(); const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections await myObject.save({ key: 'stringField' }); await myObject.destroy(); diff --git a/spec/eslint.config.js b/spec/eslint.config.js index 0f6c0b11c5..e870d91642 100644 --- a/spec/eslint.config.js +++ b/spec/eslint.config.js @@ -36,6 +36,7 @@ module.exports = [ describe_only_db: "readonly", fdescribe_only_db: "readonly", describe_only: "readonly", + fdescribe_only: "readonly", on_db: "readonly", defaultConfiguration: "readonly", range: "readonly", diff --git a/spec/helper.js b/spec/helper.js index 7deb5c495e..0e0ec6a4f1 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -1,9 +1,11 @@ 'use strict'; const dns = require('dns'); const semver = require('semver'); +const Parse = require('parse/node'); const CurrentSpecReporter = require('./support/CurrentSpecReporter.js'); const { SpecReporter } = require('jasmine-spec-reporter'); const SchemaCache = require('../lib/Adapters/Cache/SchemaCache').default; +const { sleep, Connections } = require('../lib/TestUtils'); // Ensure localhost resolves to ipv4 address first on node v17+ if (dns.setDefaultResultOrder) { @@ -53,7 +55,6 @@ const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase' const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; let databaseAdapter; let databaseURI; -// need to bind for mocking mocha if (process.env.PARSE_SERVER_DATABASE_ADAPTER) { databaseAdapter = JSON.parse(process.env.PARSE_SERVER_DATABASE_ADAPTER); @@ -73,7 +74,7 @@ if (process.env.PARSE_SERVER_DATABASE_ADAPTER) { } const port = 8378; - +const serverURL = `http://localhost:${port}/1`; let filesAdapter; on_db( @@ -99,7 +100,7 @@ if (process.env.PARSE_SERVER_LOG_LEVEL) { // Default server configuration for tests. const defaultConfiguration = { filesAdapter, - serverURL: 'http://localhost:' + port + '/1', + serverURL, databaseAdapter, appId: 'test', javascriptKey: 'test', @@ -153,34 +154,38 @@ if (silent) { }; } -if (process.env.PARSE_SERVER_TEST_CACHE === 'redis') { - defaultConfiguration.cacheAdapter = new RedisCacheAdapter(); -} - -const openConnections = {}; -const destroyAliveConnections = function () { - for (const socketId in openConnections) { - try { - openConnections[socketId].destroy(); - delete openConnections[socketId]; - } catch (e) { - /* */ - } - } -}; // Set up a default API server for testing with default configuration. let parseServer; let didChangeConfiguration = false; +const openConnections = new Connections(); + +const shutdownServer = async (_parseServer) => { + await _parseServer.handleShutdown(); + // Connection close events are not immediate on node 10+, so wait a bit + await sleep(0); + expect(openConnections.count() > 0).toBeFalsy(`There were ${openConnections.count()} open connections to the server left after the test finished`); + parseServer = undefined; +}; // Allows testing specific configurations of Parse Server const reconfigureServer = async (changedConfiguration = {}) => { if (parseServer) { - destroyAliveConnections(); - await new Promise(resolve => parseServer.server.close(resolve)); - parseServer = undefined; + await shutdownServer(parseServer); return reconfigureServer(changedConfiguration); } didChangeConfiguration = Object.keys(changedConfiguration).length !== 0; + databaseAdapter = new databaseAdapter.constructor({ + uri: databaseURI, + collectionPrefix: 'test_', + }); + defaultConfiguration.databaseAdapter = databaseAdapter; + global.databaseAdapter = databaseAdapter; + if (filesAdapter instanceof GridFSBucketAdapter) { + defaultConfiguration.filesAdapter = new GridFSBucketAdapter(mongoURI); + } + if (process.env.PARSE_SERVER_TEST_CACHE === 'redis') { + defaultConfiguration.cacheAdapter = new RedisCacheAdapter(); + } const newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, { mountPath: '/1', port, @@ -192,39 +197,19 @@ const reconfigureServer = async (changedConfiguration = {}) => { console.error(err); fail('should not call next'); }); - parseServer.liveQueryServer?.server?.on('connection', connection => { - const key = `${connection.remoteAddress}:${connection.remotePort}`; - openConnections[key] = connection; - connection.on('close', () => { - delete openConnections[key]; - }); - }); - parseServer.server.on('connection', connection => { - const key = `${connection.remoteAddress}:${connection.remotePort}`; - openConnections[key] = connection; - connection.on('close', () => { - delete openConnections[key]; - }); - }); + openConnections.track(parseServer.server); + if (parseServer.liveQueryServer?.server && parseServer.liveQueryServer.server !== parseServer.server) { + openConnections.track(parseServer.liveQueryServer.server); + } return parseServer; }; -// Set up a Parse client to talk to our test API server -const Parse = require('parse/node'); -Parse.serverURL = 'http://localhost:' + port + '/1'; - beforeAll(async () => { - try { - Parse.User.enableUnsafeCurrentUser(); - } catch (error) { - if (error !== 'You need to call Parse.initialize before using Parse.') { - throw error; - } - } await reconfigureServer(); - Parse.initialize('test', 'test', 'test'); - Parse.serverURL = 'http://localhost:' + port + '/1'; + Parse.serverURL = serverURL; + Parse.User.enableUnsafeCurrentUser(); + Parse.CoreManager.set('REQUEST_ATTEMPT_LIMIT', 1); }); global.afterEachFn = async () => { @@ -253,19 +238,7 @@ global.afterEachFn = async () => { }, }); }); - await Parse.User.logOut().catch(() => {}); - - // Connection close events are not immediate on node 10+, so wait a bit - await new Promise(resolve => setTimeout(resolve, 0)); - - // After logout operations - if (Object.keys(openConnections).length > 1) { - console.warn( - `There were ${Object.keys(openConnections).length} open connections to the server left after the test finished` - ); - } - await TestUtils.destroyAllDataPermanently(true); SchemaCache.clear(); @@ -454,6 +427,7 @@ global.mockCustomAuthenticator = mockCustomAuthenticator; global.mockFacebookAuthenticator = mockFacebookAuthenticator; global.databaseAdapter = databaseAdapter; global.databaseURI = databaseURI; +global.shutdownServer = shutdownServer; global.jfail = function (err) { fail(JSON.stringify(err)); }; @@ -610,6 +584,14 @@ global.describe_only = validator => { } }; +global.fdescribe_only = validator => { + if (validator()) { + return fdescribe; + } else { + return xdescribe; + } +}; + const libraryCache = {}; jasmine.mockLibrary = function (library, name, mock) { const original = require(library)[name]; diff --git a/spec/index.spec.js b/spec/index.spec.js index 1a2ea889a9..5093a6ea25 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -62,6 +62,7 @@ describe('server', () => { }); it('fails if database is unreachable', async () => { + spyOn(console, 'error').and.callFake(() => {}); const server = new ParseServer.default({ ...defaultConfiguration, databaseAdapter: new MongoStorageAdapter({ @@ -145,7 +146,7 @@ describe('server', () => { }, publicServerURL: 'http://localhost:8378/1', }; - expectAsync(reconfigureServer(options)).toBeRejected('MockMailAdapterConstructor'); + await expectAsync(reconfigureServer(options)).toBeRejected('MockMailAdapterConstructor'); }); }); diff --git a/spec/support/CurrentSpecReporter.js b/spec/support/CurrentSpecReporter.js index 8e0e0dafd1..626517cdf4 100755 --- a/spec/support/CurrentSpecReporter.js +++ b/spec/support/CurrentSpecReporter.js @@ -20,6 +20,8 @@ const flakyTests = [ "UserController sendVerificationEmail parseFrameURL provided uses parseFrameURL and includes the destination in the link parameter", // Expected undefined to be defined "Email Verification Token Expiration: sets the _email_verify_token_expires_at and _email_verify_token fields after user SignUp", + // Expected 0 to be 1. + "Email Verification Token Expiration: should send a new verification email when a resend is requested and the user is UNVERIFIED", ]; /** The minimum execution time in seconds for a test to be considered slow. */ diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index 004ef46810..5748f7f10f 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -97,14 +97,22 @@ class ParseLiveQueryServer { if (this.subscriber.isOpen) { await Promise.all([ ...[...this.clients.values()].map(client => client.parseWebSocket.ws.close()), - this.parseWebSocketServer.close(), - ...Array.from(this.subscriber.subscriptions.keys()).map(key => + this.parseWebSocketServer.close?.(), + ...Array.from(this.subscriber.subscriptions?.keys() || []).map(key => this.subscriber.unsubscribe(key) ), this.subscriber.close?.(), ]); } - this.subscriber.isOpen = false; + if (typeof this.subscriber.quit === 'function') { + try { + await this.subscriber.quit(); + } catch (err) { + logger.error('PubSubAdapter error on shutdown', { error: err }); + } + } else { + this.subscriber.isOpen = false; + } } _createSubscribers() { diff --git a/src/ParseServer.js b/src/ParseServer.js index 6a693e3145..7b09a8c335 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -45,10 +45,14 @@ import CheckRunner from './Security/CheckRunner'; import Deprecator from './Deprecator/Deprecator'; import { DefinedSchemas } from './SchemaMigrations/DefinedSchemas'; import OptionsDefinitions from './Options/Definitions'; +import { resolvingPromise, Connections } from './TestUtils'; // Mutate the Parse object to add the Cloud Code handlers addParseCloud(); +// Track connections to destroy them on shutdown +const connections = new Connections(); + // ParseServer works like a constructor of an express app. // https://parseplatform.org/parse-server/api/master/ParseServerOptions.html class ParseServer { @@ -214,8 +218,39 @@ class ParseServer { return this._app; } - handleShutdown() { + /** + * Stops the parse server, cancels any ongoing requests and closes all connections. + * + * Currently, express doesn't shut down immediately after receiving SIGINT/SIGTERM + * if it has client connections that haven't timed out. + * (This is a known issue with node - https://github.com/nodejs/node/issues/2642) + * + * @returns {Promise} a promise that resolves when the server is stopped + */ + async handleShutdown() { + const serverClosePromise = resolvingPromise(); + const liveQueryServerClosePromise = resolvingPromise(); const promises = []; + this.server.close((error) => { + /* istanbul ignore next */ + if (error) { + // eslint-disable-next-line no-console + console.error('Error while closing parse server', error); + } + serverClosePromise.resolve(); + }); + if (this.liveQueryServer?.server?.close && this.liveQueryServer.server !== this.server) { + this.liveQueryServer.server.close((error) => { + /* istanbul ignore next */ + if (error) { + // eslint-disable-next-line no-console + console.error('Error while closing live query server', error); + } + liveQueryServerClosePromise.resolve(); + }); + } else { + liveQueryServerClosePromise.resolve(); + } const { adapter: databaseAdapter } = this.config.databaseController; if (databaseAdapter && typeof databaseAdapter.handleShutdown === 'function') { promises.push(databaseAdapter.handleShutdown()); @@ -228,17 +263,15 @@ class ParseServer { if (cacheAdapter && typeof cacheAdapter.handleShutdown === 'function') { promises.push(cacheAdapter.handleShutdown()); } - if (this.liveQueryServer?.server?.close) { - promises.push(new Promise(resolve => this.liveQueryServer.server.close(resolve))); - } if (this.liveQueryServer) { promises.push(this.liveQueryServer.shutdown()); } - return (promises.length > 0 ? Promise.all(promises) : Promise.resolve()).then(() => { - if (this.config.serverCloseComplete) { - this.config.serverCloseComplete(); - } - }); + await Promise.all(promises); + connections.destroyAll(); + await Promise.all([serverClosePromise, liveQueryServerClosePromise]); + if (this.config.serverCloseComplete) { + this.config.serverCloseComplete(); + } } /** @@ -419,6 +452,7 @@ class ParseServer { }); }); this.server = server; + connections.track(server); if (options.startLiveQueryServer || options.liveQueryServerOptions) { this.liveQueryServer = await ParseServer.createLiveQueryServer( @@ -426,6 +460,9 @@ class ParseServer { options.liveQueryServerOptions, options ); + if (this.liveQueryServer.server !== this.server) { + connections.track(this.liveQueryServer.server); + } } if (options.trustProxy) { app.set('trust proxy', options.trustProxy); @@ -600,32 +637,8 @@ function injectDefaults(options: ParseServerOptions) { // Those can't be tested as it requires a subprocess /* istanbul ignore next */ function configureListeners(parseServer) { - const server = parseServer.server; - const sockets = {}; - /* Currently, express doesn't shut down immediately after receiving SIGINT/SIGTERM if it has client connections that haven't timed out. (This is a known issue with node - https://github.com/nodejs/node/issues/2642) - This function, along with `destroyAliveConnections()`, intend to fix this behavior such that parse server will close all open connections and initiate the shutdown process as soon as it receives a SIGINT/SIGTERM signal. */ - server.on('connection', socket => { - const socketId = socket.remoteAddress + ':' + socket.remotePort; - sockets[socketId] = socket; - socket.on('close', () => { - delete sockets[socketId]; - }); - }); - - const destroyAliveConnections = function () { - for (const socketId in sockets) { - try { - sockets[socketId].destroy(); - } catch (e) { - /* */ - } - } - }; - const handleShutdown = function () { process.stdout.write('Termination signal received. Shutting down.'); - destroyAliveConnections(); - server.close(); parseServer.handleShutdown(); }; process.on('SIGTERM', handleShutdown); diff --git a/src/TestUtils.js b/src/TestUtils.js index 912a459519..2cd1493511 100644 --- a/src/TestUtils.js +++ b/src/TestUtils.js @@ -42,3 +42,42 @@ export function resolvingPromise() { export function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } + +export function getConnectionsCount(server) { + return new Promise((resolve, reject) => { + server.getConnections((err, count) => { + /* istanbul ignore next */ + if (err) { + reject(err); + } else { + resolve(count); + } + }); + }); +}; + +export class Connections { + constructor() { + this.sockets = new Set(); + } + + track(server) { + server.on('connection', socket => { + this.sockets.add(socket); + socket.on('close', () => { + this.sockets.delete(socket); + }); + }); + } + + destroyAll() { + for (const socket of this.sockets.values()) { + socket.destroy(); + } + this.sockets.clear(); + } + + count() { + return this.sockets.size; + } +}