fix: Parse Server doesn't shutdown gracefully (#9634)

This commit is contained in:
Diamond Lewis
2025-03-27 15:38:51 -05:00
committed by GitHub
parent f55de2b342
commit aed918d310
19 changed files with 308 additions and 240 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -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();

View File

@@ -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',

View File

@@ -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: {

View File

@@ -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',
},
});

View File

@@ -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;

View File

@@ -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();

View File

@@ -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

View File

@@ -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();
});

View File

@@ -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;
});
});
});

View File

@@ -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();

View File

@@ -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",

View File

@@ -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];

View File

@@ -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');
});
});

View File

@@ -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. */