From 1877ceda0399b950f505c9058c2e2f380994c3fd Mon Sep 17 00:00:00 2001
From: Diamond Lewis <findlewis@gmail.com>
Date: Mon, 10 Mar 2025 19:47:46 -0500
Subject: [PATCH 1/4] fix: Parse server doesn't shutdown gracefully

---
 spec/EnableExpressErrorHandler.spec.js |   8 +-
 spec/ParseAPI.spec.js                  |   2 -
 spec/ParseConfigKey.spec.js            |   6 +-
 spec/ParseGraphQLServer.spec.js        |  47 +++++++----
 spec/ParseLiveQuery.spec.js            |  76 +++++++++++++++++-
 spec/ParseQuery.FullTextSearch.spec.js |  19 ++---
 spec/ParseServer.spec.js               |  46 -----------
 spec/ParseServerRESTController.spec.js |   1 -
 spec/PushController.spec.js            |  94 ++++++++++------------
 spec/SchemaPerformance.spec.js         |   4 +-
 spec/SecurityCheckGroups.spec.js       |   4 +
 spec/batch.spec.js                     |   1 -
 spec/eslint.config.js                  |   1 +
 spec/helper.js                         | 107 +++++++++++--------------
 spec/index.spec.js                     |   3 +-
 spec/support/CurrentSpecReporter.js    |   2 +
 src/LiveQuery/ParseLiveQueryServer.js  |  14 +++-
 src/ParseServer.js                     |  74 ++++++++++-------
 src/TestUtils.js                       |  38 +++++++++
 19 files changed, 311 insertions(+), 236 deletions(-)

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 875825025d..028ce4524d 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 728b88804c..047a57dac9 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 43d523c214..ac2c85a433 100644
--- a/spec/SecurityCheckGroups.spec.js
+++ b/spec/SecurityCheckGroups.spec.js
@@ -64,18 +64,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 4d423bda67..caab840f0c 100644
--- a/spec/eslint.config.js
+++ b/spec/eslint.config.js
@@ -35,6 +35,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 bc20ebcf5e..a10798b915 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 { resolvingPromise, sleep, Connections } = require('../lib/TestUtils');
 
 // Ensure localhost resolves to ipv4 address first on node v17+
 if (dns.setDefaultResultOrder) {
@@ -73,7 +75,7 @@ if (process.env.PARSE_SERVER_DATABASE_ADAPTER) {
 }
 
 const port = 8378;
-
+const serverURL = `http://localhost:${port}/1`;
 let filesAdapter;
 
 on_db(
@@ -99,7 +101,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 +155,46 @@ if (silent) {
   };
 }
 
-if (process.env.PARSE_SERVER_TEST_CACHE === 'redis') {
-  defaultConfiguration.cacheAdapter = new RedisCacheAdapter();
-}
+// Set up a default API server for testing with default configuration.
+let parseServer;
+let didChangeConfiguration = false;
+const openConnections = new Connections();
 
-const openConnections = {};
-const destroyAliveConnections = function () {
-  for (const socketId in openConnections) {
-    try {
-      openConnections[socketId].destroy();
-      delete openConnections[socketId];
-    } catch (e) {
-      /* */
+const shutdownServer = async (_parseServer) => {
+  _parseServer.handleShutdown();
+  parseServer = undefined;
+  // Connection close events are not immediate on node 10+, so wait a bit
+  await sleep(0);
+  // Jasmine process counts as one open connection 
+  const connectionMessage = `There were ${openConnections.count()} open connections to the server left after the test finished`;
+  if (process.env.PARSE_SERVER_TEST_CACHE === 'redis') {
+    if (openConnections.count() > 1) {
+      console.log(connectionMessage);
     }
+  } else {
+    expect(openConnections.count() > 1).toBeFalsy(connectionMessage);
   }
 };
-// Set up a default API server for testing with default configuration.
-let parseServer;
-let didChangeConfiguration = false;
 
 // 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 +206,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 +247,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();
 
@@ -434,6 +416,7 @@ global.mockCustomAuthenticator = mockCustomAuthenticator;
 global.mockFacebookAuthenticator = mockFacebookAuthenticator;
 global.databaseAdapter = databaseAdapter;
 global.databaseURI = databaseURI;
+global.shutdownServer = shutdownServer;
 global.jfail = function (err) {
   fail(JSON.stringify(err));
 };
@@ -590,6 +573,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..15fa9c2f5c 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,20 @@ 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<void>} a promise that resolves when the server is stopped
+   */
+  async handleShutdown() {
+    const serverClosePromise = resolvingPromise();
+    const liveQueryServerClosePromise = resolvingPromise();
     const promises = [];
+
     const { adapter: databaseAdapter } = this.config.databaseController;
     if (databaseAdapter && typeof databaseAdapter.handleShutdown === 'function') {
       promises.push(databaseAdapter.handleShutdown());
@@ -228,17 +244,33 @@ 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);
+    this.server.close((error) => {
+      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) => {
+        if (error) {
+          // eslint-disable-next-line no-console
+          console.error('Error while closing live query server', error);
+        }
+        liveQueryServerClosePromise.resolve();
+      });
+    } else {
+      liveQueryServerClosePromise.resolve();
+    }
+    connections.destroyAll();
+    await Promise.all([serverClosePromise, liveQueryServerClosePromise]);
+    if (this.config.serverCloseComplete) {
+      this.config.serverCloseComplete();
+    }
   }
 
   /**
@@ -419,6 +451,7 @@ class ParseServer {
       });
     });
     this.server = server;
+    connections.track(server);
 
     if (options.startLiveQueryServer || options.liveQueryServerOptions) {
       this.liveQueryServer = await ParseServer.createLiveQueryServer(
@@ -426,6 +459,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 +636,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..2d7b7d6f00 100644
--- a/src/TestUtils.js
+++ b/src/TestUtils.js
@@ -42,3 +42,41 @@ 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) => {
+      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;
+  }
+}

From b94ec3ef773630e272adda257cffa8eb2c02490f Mon Sep 17 00:00:00 2001
From: Diamond Lewis <findlewis@gmail.com>
Date: Tue, 11 Mar 2025 12:13:59 -0500
Subject: [PATCH 2/4] immediately stop accepting requests

---
 src/ParseServer.js | 33 ++++++++++++++++-----------------
 1 file changed, 16 insertions(+), 17 deletions(-)

diff --git a/src/ParseServer.js b/src/ParseServer.js
index 15fa9c2f5c..2b3609e35a 100644
--- a/src/ParseServer.js
+++ b/src/ParseServer.js
@@ -231,23 +231,6 @@ class ParseServer {
     const serverClosePromise = resolvingPromise();
     const liveQueryServerClosePromise = resolvingPromise();
     const promises = [];
-
-    const { adapter: databaseAdapter } = this.config.databaseController;
-    if (databaseAdapter && typeof databaseAdapter.handleShutdown === 'function') {
-      promises.push(databaseAdapter.handleShutdown());
-    }
-    const { adapter: fileAdapter } = this.config.filesController;
-    if (fileAdapter && typeof fileAdapter.handleShutdown === 'function') {
-      promises.push(fileAdapter.handleShutdown());
-    }
-    const { adapter: cacheAdapter } = this.config.cacheController;
-    if (cacheAdapter && typeof cacheAdapter.handleShutdown === 'function') {
-      promises.push(cacheAdapter.handleShutdown());
-    }
-    if (this.liveQueryServer) {
-      promises.push(this.liveQueryServer.shutdown());
-    }
-    await Promise.all(promises);
     this.server.close((error) => {
       if (error) {
         // eslint-disable-next-line no-console
@@ -266,6 +249,22 @@ class ParseServer {
     } else {
       liveQueryServerClosePromise.resolve();
     }
+    const { adapter: databaseAdapter } = this.config.databaseController;
+    if (databaseAdapter && typeof databaseAdapter.handleShutdown === 'function') {
+      promises.push(databaseAdapter.handleShutdown());
+    }
+    const { adapter: fileAdapter } = this.config.filesController;
+    if (fileAdapter && typeof fileAdapter.handleShutdown === 'function') {
+      promises.push(fileAdapter.handleShutdown());
+    }
+    const { adapter: cacheAdapter } = this.config.cacheController;
+    if (cacheAdapter && typeof cacheAdapter.handleShutdown === 'function') {
+      promises.push(cacheAdapter.handleShutdown());
+    }
+    if (this.liveQueryServer) {
+      promises.push(this.liveQueryServer.shutdown());
+    }
+    await Promise.all(promises);
     connections.destroyAll();
     await Promise.all([serverClosePromise, liveQueryServerClosePromise]);
     if (this.config.serverCloseComplete) {

From 4c99ab9a4497af8dee485580cb77e6f1d85ae481 Mon Sep 17 00:00:00 2001
From: Diamond Lewis <findlewis@gmail.com>
Date: Tue, 11 Mar 2025 18:20:34 -0500
Subject: [PATCH 3/4] I'm a moron

---
 spec/helper.js | 17 ++++-------------
 1 file changed, 4 insertions(+), 13 deletions(-)

diff --git a/spec/helper.js b/spec/helper.js
index a10798b915..1fb1ef547f 100644
--- a/spec/helper.js
+++ b/spec/helper.js
@@ -5,7 +5,7 @@ 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 { resolvingPromise, sleep, Connections } = require('../lib/TestUtils');
+const { sleep, Connections } = require('../lib/TestUtils');
 
 // Ensure localhost resolves to ipv4 address first on node v17+
 if (dns.setDefaultResultOrder) {
@@ -55,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);
@@ -161,19 +160,11 @@ let didChangeConfiguration = false;
 const openConnections = new Connections();
 
 const shutdownServer = async (_parseServer) => {
-  _parseServer.handleShutdown();
-  parseServer = undefined;
+  await _parseServer.handleShutdown();
   // Connection close events are not immediate on node 10+, so wait a bit
   await sleep(0);
-  // Jasmine process counts as one open connection 
-  const connectionMessage = `There were ${openConnections.count()} open connections to the server left after the test finished`;
-  if (process.env.PARSE_SERVER_TEST_CACHE === 'redis') {
-    if (openConnections.count() > 1) {
-      console.log(connectionMessage);
-    }
-  } else {
-    expect(openConnections.count() > 1).toBeFalsy(connectionMessage);
-  }
+  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

From db173948b606812d511fa8927f1934adf4d812f6 Mon Sep 17 00:00:00 2001
From: Diamond Lewis <findlewis@gmail.com>
Date: Tue, 11 Mar 2025 22:28:31 -0500
Subject: [PATCH 4/4] improve coverage

---
 src/ParseServer.js | 2 ++
 src/TestUtils.js   | 1 +
 2 files changed, 3 insertions(+)

diff --git a/src/ParseServer.js b/src/ParseServer.js
index 2b3609e35a..7b09a8c335 100644
--- a/src/ParseServer.js
+++ b/src/ParseServer.js
@@ -232,6 +232,7 @@ class ParseServer {
     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);
@@ -240,6 +241,7 @@ class ParseServer {
     });
     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);
diff --git a/src/TestUtils.js b/src/TestUtils.js
index 2d7b7d6f00..2cd1493511 100644
--- a/src/TestUtils.js
+++ b/src/TestUtils.js
@@ -46,6 +46,7 @@ export function sleep(ms) {
 export function getConnectionsCount(server) {
   return new Promise((resolve, reject) => {
     server.getConnections((err, count) => {
+      /* istanbul ignore next */
       if (err) {
         reject(err);
       } else {