Răsfoiți Sursa

Merge pull request #27936 from overleaf/em-unit-tests-mongo

Make Mongo available to unit tests in all services

GitOrigin-RevId: b65bbb69883d5bba31d09802b89f35bdc523fe4d
Eric Mc Sween 11 luni în urmă
părinte
comite
8f0913fafe
57 a modificat fișierele cu 404 adăugiri și 162 ștergeri
  1. 5 0
      libraries/mongo-utils/index.js
  2. 55 0
      libraries/mongo-utils/test-utils.js
  3. 30 0
      libraries/redis-wrapper/index.js
  4. 7 0
      package-lock.json
  5. 0 1
      services/chat/Makefile
  6. 7 0
      services/chat/app/js/mongodb.js
  7. 7 0
      services/chat/docker-compose.ci.yml
  8. 6 0
      services/chat/docker-compose.yml
  9. 2 1
      services/chat/package.json
  10. 0 1
      services/clsi/Makefile
  11. 1 0
      services/clsi/docker-compose.ci.yml
  12. 1 0
      services/clsi/docker-compose.yml
  13. 1 1
      services/clsi/package.json
  14. 0 1
      services/contacts/Makefile
  15. 7 0
      services/contacts/app/js/mongodb.js
  16. 7 0
      services/contacts/docker-compose.ci.yml
  17. 6 0
      services/contacts/docker-compose.yml
  18. 2 1
      services/contacts/package.json
  19. 20 55
      services/contacts/test/unit/js/ContactsManagerTests.js
  20. 0 1
      services/docstore/Makefile
  21. 8 0
      services/docstore/app/js/mongodb.js
  22. 7 0
      services/docstore/docker-compose.ci.yml
  23. 6 0
      services/docstore/docker-compose.yml
  24. 2 1
      services/docstore/package.json
  25. 0 1
      services/document-updater/Makefile
  26. 7 3
      services/document-updater/app/js/RedisManager.js
  27. 8 0
      services/document-updater/app/js/mongodb.js
  28. 19 6
      services/document-updater/docker-compose.ci.yml
  29. 18 6
      services/document-updater/docker-compose.yml
  30. 2 1
      services/document-updater/package.json
  31. 0 1
      services/filestore/Makefile
  32. 1 0
      services/filestore/docker-compose.ci.yml
  33. 1 0
      services/filestore/docker-compose.yml
  34. 1 1
      services/filestore/package.json
  35. 0 1
      services/history-v1/Makefile
  36. 19 6
      services/history-v1/docker-compose.ci.yml
  37. 18 6
      services/history-v1/docker-compose.yml
  38. 1 1
      services/history-v1/package.json
  39. 8 0
      services/history-v1/storage/lib/mongodb.js
  40. 7 0
      services/history-v1/storage/lib/redis.js
  41. 0 1
      services/notifications/Makefile
  42. 8 0
      services/notifications/app/js/mongodb.js
  43. 7 0
      services/notifications/docker-compose.ci.yml
  44. 6 0
      services/notifications/docker-compose.yml
  45. 2 1
      services/notifications/package.json
  46. 0 1
      services/project-history/Makefile
  47. 5 0
      services/project-history/app/js/RedisManager.js
  48. 7 1
      services/project-history/app/js/mongodb.js
  49. 19 6
      services/project-history/docker-compose.ci.yml
  50. 18 6
      services/project-history/docker-compose.yml
  51. 2 1
      services/project-history/package.json
  52. 0 1
      services/real-time/Makefile
  53. 14 6
      services/real-time/docker-compose.ci.yml
  54. 14 6
      services/real-time/docker-compose.yml
  55. 1 1
      services/real-time/package.json
  56. 1 12
      services/web/app/src/infrastructure/RedisWrapper.js
  57. 3 23
      services/web/app/src/infrastructure/mongodb.js

+ 5 - 0
libraries/mongo-utils/index.js

@@ -0,0 +1,5 @@
+// @ts-check
+
+const { cleanupTestDatabase, dropTestDatabase } = require('./test-utils')
+
+module.exports = { cleanupTestDatabase, dropTestDatabase }

+ 55 - 0
libraries/mongo-utils/test-utils.js

@@ -0,0 +1,55 @@
+// @ts-check
+
+/**
+ * @import { MongoClient } from 'mongodb'
+ * @import { MongoClient as LegacyMongoClient } from 'mongodb-legacy'
+ */
+
+/**
+ * Delete all data from the test Mongo database
+ *
+ * This doesn't drop the collections, so indexes are preserved.
+ *
+ * @param {MongoClient | LegacyMongoClient} mongoClient
+ */
+async function cleanupTestDatabase(mongoClient) {
+  ensureTestDatabase(mongoClient)
+  const db = mongoClient.db()
+  const allCollections = await db.collections()
+  const collections = allCollections.filter(
+    coll => coll.collectionName !== 'migrations'
+  )
+  await Promise.all(collections.map(coll => coll.deleteMany({})))
+}
+
+/**
+ * Drop the test Monto database
+ *
+ * This drops the whole database, including indexes.
+ *
+ * @param {MongoClient | LegacyMongoClient } mongoClient
+ */
+async function dropTestDatabase(mongoClient) {
+  ensureTestDatabase(mongoClient)
+  await mongoClient.db().dropDatabase()
+}
+/**
+ * Ensure that the given client is connected to a test database.
+ *
+ * This should be called before performing destructive operations on the test
+ * database.
+ *
+ * @param {MongoClient | LegacyMongoClient } mongoClient
+ */
+function ensureTestDatabase(mongoClient) {
+  const dbName = mongoClient.db().databaseName
+  const env = process.env.NODE_ENV
+
+  if (dbName !== 'test-overleaf' || env !== 'test') {
+    throw new Error(
+      `Refusing to clear database '${dbName}' in environment '${env}'`
+    )
+  }
+}
+
+module.exports = { cleanupTestDatabase, dropTestDatabase }

+ 30 - 0
libraries/redis-wrapper/index.js

@@ -1,3 +1,5 @@
+// @ts-check
+
 const crypto = require('node:crypto')
 const os = require('node:os')
 const { promisify } = require('node:util')
@@ -168,6 +170,34 @@ async function runWithTimeout({ runner, timeout, context }) {
   ])
 }
 
+/**
+ * Delete all data from the test Redis instance
+ *
+ * @param {Redis} rclient
+ */
+async function cleanupTestRedis(rclient) {
+  ensureTestRedis(rclient)
+  await rclient.flushall()
+}
+
+/**
+ * Checks that the Redis client points to a test database
+ *
+ * In tests, the Redis instance is on a host called redis_test
+ *
+ * @param {Redis} rclient
+ */
+function ensureTestRedis(rclient) {
+  const host = rclient.options.host
+  const env = process.env.NODE_ENV
+  if (host !== 'redis_test' || env !== 'test') {
+    throw new Error(
+      `Refusing to clear Redis instance '${host}' in environment '${env}'`
+    )
+  }
+}
+
 module.exports = {
   createClient,
+  cleanupTestRedis,
 }

+ 7 - 0
package-lock.json

@@ -42339,6 +42339,7 @@
       "dependencies": {
         "@overleaf/logger": "*",
         "@overleaf/metrics": "*",
+        "@overleaf/mongo-utils": "*",
         "@overleaf/settings": "*",
         "async": "^3.2.5",
         "body-parser": "^1.20.3",
@@ -42491,6 +42492,7 @@
       "dependencies": {
         "@overleaf/logger": "*",
         "@overleaf/metrics": "*",
+        "@overleaf/mongo-utils": "*",
         "@overleaf/settings": "*",
         "async": "^3.2.5",
         "body-parser": "^1.20.3",
@@ -42556,6 +42558,7 @@
         "@overleaf/fetch-utils": "*",
         "@overleaf/logger": "*",
         "@overleaf/metrics": "*",
+        "@overleaf/mongo-utils": "*",
         "@overleaf/o-error": "*",
         "@overleaf/object-persistor": "*",
         "@overleaf/promise-utils": "*",
@@ -42627,6 +42630,7 @@
       "dependencies": {
         "@overleaf/logger": "*",
         "@overleaf/metrics": "*",
+        "@overleaf/mongo-utils": "*",
         "@overleaf/o-error": "*",
         "@overleaf/promise-utils": "*",
         "@overleaf/ranges-tracker": "*",
@@ -44089,6 +44093,7 @@
       "dependencies": {
         "@overleaf/logger": "*",
         "@overleaf/metrics": "*",
+        "@overleaf/mongo-utils": "*",
         "@overleaf/settings": "*",
         "async": "^3.2.5",
         "body-parser": "^1.20.3",
@@ -44113,6 +44118,7 @@
         "@overleaf/fetch-utils": "*",
         "@overleaf/logger": "*",
         "@overleaf/metrics": "*",
+        "@overleaf/mongo-utils": "*",
         "@overleaf/o-error": "*",
         "@overleaf/promise-utils": "*",
         "@overleaf/redis-wrapper": "*",
@@ -44341,6 +44347,7 @@
         "@overleaf/fetch-utils": "*",
         "@overleaf/logger": "*",
         "@overleaf/metrics": "*",
+        "@overleaf/mongo-utils": "*",
         "@overleaf/o-error": "*",
         "@overleaf/settings": "*",
         "async": "^3.2.5",

+ 0 - 1
services/chat/Makefile

@@ -86,7 +86,6 @@ test: format lint typecheck shellcheck test_unit test_acceptance
 test_unit:
 ifneq (,$(wildcard test/unit))
 	$(DOCKER_COMPOSE_TEST_UNIT) run --rm test_unit
-	$(MAKE) test_unit_clean
 endif
 
 test_clean: test_unit_clean

+ 7 - 0
services/chat/app/js/mongodb.js

@@ -1,5 +1,8 @@
+// @ts-check
+
 import Metrics from '@overleaf/metrics'
 import Settings from '@overleaf/settings'
+import MongoUtils from '@overleaf/mongo-utils'
 import { MongoClient } from 'mongodb'
 
 export { ObjectId } from 'mongodb'
@@ -16,3 +19,7 @@ export const db = {
 }
 
 Metrics.mongodb.monitor(mongoClient)
+
+export async function cleanupTestDatabase() {
+  await MongoUtils.cleanupTestDatabase(mongoClient)
+}

+ 7 - 0
services/chat/docker-compose.ci.yml

@@ -6,10 +6,17 @@ services:
   test_unit:
     image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
     user: node
+    volumes:
+      - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
+    entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: npm run test:unit:_run
     environment:
+      MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
+    depends_on:
+      mongo:
+        condition: service_started
 
 
   test_acceptance:

+ 6 - 0
services/chat/docker-compose.yml

@@ -9,14 +9,20 @@ services:
       - .:/overleaf/services/chat
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
     working_dir: /overleaf/services/chat
     environment:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
+      MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
+    entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: npm run --silent test:unit
     user: node
+    depends_on:
+      mongo:
+        condition: service_started
 
   test_acceptance:
     image: node:22.18.0

+ 2 - 1
services/chat/package.json

@@ -10,7 +10,7 @@
     "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
     "nodemon": "node --watch app.js",
     "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
-    "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js",
+    "test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js",
     "lint": "eslint --max-warnings 0 --format unix .",
     "format": "prettier --list-different $PWD/'**/*.*js'",
     "format:fix": "prettier --write $PWD/'**/*.*js'",
@@ -20,6 +20,7 @@
   "dependencies": {
     "@overleaf/logger": "*",
     "@overleaf/metrics": "*",
+    "@overleaf/mongo-utils": "*",
     "@overleaf/settings": "*",
     "async": "^3.2.5",
     "body-parser": "^1.20.3",

+ 0 - 1
services/clsi/Makefile

@@ -87,7 +87,6 @@ test: format lint typecheck shellcheck test_unit test_acceptance
 test_unit:
 ifneq (,$(wildcard test/unit))
 	$(DOCKER_COMPOSE_TEST_UNIT) run --rm test_unit
-	$(MAKE) test_unit_clean
 endif
 
 test_clean: test_unit_clean

+ 1 - 0
services/clsi/docker-compose.ci.yml

@@ -7,6 +7,7 @@ services:
     image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
     command: npm run test:unit:_run
     environment:
+      MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
 

+ 1 - 0
services/clsi/docker-compose.yml

@@ -16,6 +16,7 @@ services:
     environment:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
+      MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
     command: npm run --silent test:unit

+ 1 - 1
services/clsi/package.json

@@ -7,7 +7,7 @@
     "start": "node app.js",
     "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
     "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
-    "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js",
+    "test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js",
     "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
     "nodemon": "node --watch app.js",
     "lint": "eslint --max-warnings 0 --format unix .",

+ 0 - 1
services/contacts/Makefile

@@ -86,7 +86,6 @@ test: format lint typecheck shellcheck test_unit test_acceptance
 test_unit:
 ifneq (,$(wildcard test/unit))
 	$(DOCKER_COMPOSE_TEST_UNIT) run --rm test_unit
-	$(MAKE) test_unit_clean
 endif
 
 test_clean: test_unit_clean

+ 7 - 0
services/contacts/app/js/mongodb.js

@@ -1,4 +1,7 @@
+// @ts-check
+
 import Metrics from '@overleaf/metrics'
+import MongoUtils from '@overleaf/mongo-utils'
 import Settings from '@overleaf/settings'
 import { MongoClient } from 'mongodb'
 
@@ -15,3 +18,7 @@ export const db = {
 }
 
 Metrics.mongodb.monitor(mongoClient)
+
+export async function cleanupTestDatabase() {
+  await MongoUtils.cleanupTestDatabase(mongoClient)
+}

+ 7 - 0
services/contacts/docker-compose.ci.yml

@@ -6,10 +6,17 @@ services:
   test_unit:
     image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
     user: node
+    volumes:
+      - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
+    entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: npm run test:unit:_run
     environment:
+      MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
+    depends_on:
+      mongo:
+        condition: service_started
 
 
   test_acceptance:

+ 6 - 0
services/contacts/docker-compose.yml

@@ -9,14 +9,20 @@ services:
       - .:/overleaf/services/contacts
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
     working_dir: /overleaf/services/contacts
     environment:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
+      MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
+    entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: npm run --silent test:unit
     user: node
+    depends_on:
+      mongo:
+        condition: service_started
 
   test_acceptance:
     image: node:22.18.0

+ 2 - 1
services/contacts/package.json

@@ -8,7 +8,7 @@
     "start": "node app.js",
     "test:acceptance:_run": "mocha --loader=esmock --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
     "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
-    "test:unit:_run": "mocha --loader=esmock --recursive --reporter spec $@ test/unit/js",
+    "test:unit:_run": "mocha --loader=esmock --recursive --reporter spec --exit $@ test/unit/js",
     "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
     "nodemon": "node --watch app.js",
     "lint": "eslint --max-warnings 0 --format unix .",
@@ -20,6 +20,7 @@
   "dependencies": {
     "@overleaf/logger": "*",
     "@overleaf/metrics": "*",
+    "@overleaf/mongo-utils": "*",
     "@overleaf/settings": "*",
     "async": "^3.2.5",
     "body-parser": "^1.20.3",

+ 20 - 55
services/contacts/test/unit/js/ContactsManagerTests.js

@@ -1,21 +1,16 @@
+// @ts-check
+
 import sinon from 'sinon'
 import { expect } from 'chai'
-import esmock from 'esmock'
 import { ObjectId } from 'mongodb'
+import { cleanupTestDatabase } from '../../../app/js/mongodb.js'
+import * as ContactManager from '../../../app/js/ContactManager.js'
 
 describe('ContactManager', function () {
+  beforeEach(cleanupTestDatabase)
+
   beforeEach(async function () {
     this.clock = sinon.useFakeTimers(new Date())
-
-    this.db = { contacts: {} }
-
-    this.ContactManager = await esmock('../../../app/js/ContactManager', {
-      '../../../app/js/mongodb': {
-        db: this.db,
-        ObjectId,
-      },
-    })
-
     this.user_id = new ObjectId().toString()
     this.contact_id = new ObjectId().toString()
   })
@@ -25,42 +20,24 @@ describe('ContactManager', function () {
   })
 
   describe('touchContact', function () {
-    beforeEach(function () {
-      this.db.contacts.updateOne = sinon.stub().resolves()
-    })
-
     describe('with a valid user_id', function () {
       it('should increment the contact count and timestamp', async function () {
-        await expect(
-          this.ContactManager.touchContact(this.user_id, 'mock_contact')
-        ).not.to.be.rejected
-
-        expect(this.db.contacts.updateOne).to.be.calledWith(
-          {
-            user_id: sinon.match(o => o.toString() === this.user_id),
-          },
-          {
-            $inc: {
-              'contacts.mock_contact.n': 1,
-            },
-            $set: {
-              'contacts.mock_contact.ts': new Date(),
-            },
+        const now = new Date()
+        await ContactManager.touchContact(this.user_id, this.contact_id)
+        const contacts = await ContactManager.getContacts(this.user_id)
+        expect(contacts).to.deep.equal({
+          [this.contact_id]: {
+            n: 1,
+            ts: now,
           },
-          {
-            upsert: true,
-          }
-        )
+        })
       })
     })
 
     describe('with an invalid user id', function () {
       it('should be rejected', async function () {
         await expect(
-          this.ContactManager.touchContact(
-            'not-valid-object-id',
-            this.contact_id
-          )
+          ContactManager.touchContact('not-valid-object-id', this.contact_id)
         ).to.be.rejectedWith(
           'input must be a 24 character hex string, 12 byte Uint8Array, or an integer'
         )
@@ -69,29 +46,17 @@ describe('ContactManager', function () {
   })
 
   describe('getContacts', function () {
-    beforeEach(function () {
-      this.user = {
-        contacts: ['mock', 'contacts'],
-      }
-      this.db.contacts.findOne = sinon.stub().resolves(this.user)
-    })
-
     describe('with a valid user_id', function () {
-      it("should find the user's contacts", async function () {
-        await expect(
-          this.ContactManager.getContacts(this.user_id)
-        ).to.eventually.deep.equal(this.user.contacts)
-
-        expect(this.db.contacts.findOne).to.be.calledWith({
-          user_id: sinon.match(o => o.toString() === this.user_id),
-        })
+      it('should find an empty contact list', async function () {
+        const contacts = await ContactManager.getContacts(this.user_id)
+        expect(contacts).to.be.undefined
       })
     })
 
     describe('with an invalid user id', function () {
       it('should be rejected', async function () {
-        await expect(this.ContactManager.getContacts('not-valid-object-id')).to
-          .be.rejected
+        await expect(ContactManager.getContacts('not-valid-object-id')).to.be
+          .rejected
       })
     })
   })

+ 0 - 1
services/docstore/Makefile

@@ -86,7 +86,6 @@ test: format lint typecheck shellcheck test_unit test_acceptance
 test_unit:
 ifneq (,$(wildcard test/unit))
 	$(DOCKER_COMPOSE_TEST_UNIT) run --rm test_unit
-	$(MAKE) test_unit_clean
 endif
 
 test_clean: test_unit_clean

+ 8 - 0
services/docstore/app/js/mongodb.js

@@ -1,5 +1,8 @@
+// @ts-check
+
 const Metrics = require('@overleaf/metrics')
 const Settings = require('@overleaf/settings')
+const MongoUtils = require('@overleaf/mongo-utils')
 const { MongoClient, ObjectId } = require('mongodb-legacy')
 
 const mongoClient = new MongoClient(Settings.mongo.url, Settings.mongo.options)
@@ -11,8 +14,13 @@ const db = {
 
 Metrics.mongodb.monitor(mongoClient)
 
+async function cleanupTestDatabase() {
+  await MongoUtils.cleanupTestDatabase(mongoClient)
+}
+
 module.exports = {
   db,
   mongoClient,
   ObjectId,
+  cleanupTestDatabase,
 }

+ 7 - 0
services/docstore/docker-compose.ci.yml

@@ -6,10 +6,17 @@ services:
   test_unit:
     image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
     user: node
+    volumes:
+      - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
+    entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: npm run test:unit:_run
     environment:
+      MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
+    depends_on:
+      mongo:
+        condition: service_started
 
 
   test_acceptance:

+ 6 - 0
services/docstore/docker-compose.yml

@@ -9,14 +9,20 @@ services:
       - .:/overleaf/services/docstore
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
     working_dir: /overleaf/services/docstore
     environment:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
+      MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
+    entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: npm run --silent test:unit
     user: node
+    depends_on:
+      mongo:
+        condition: service_started
 
   test_acceptance:
     image: node:22.18.0

+ 2 - 1
services/docstore/package.json

@@ -7,7 +7,7 @@
     "start": "node app.js",
     "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
     "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
-    "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js",
+    "test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js",
     "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
     "nodemon": "node --watch app.js",
     "lint": "eslint --max-warnings 0 --format unix .",
@@ -20,6 +20,7 @@
     "@overleaf/fetch-utils": "*",
     "@overleaf/logger": "*",
     "@overleaf/metrics": "*",
+    "@overleaf/mongo-utils": "*",
     "@overleaf/o-error": "*",
     "@overleaf/object-persistor": "*",
     "@overleaf/promise-utils": "*",

+ 0 - 1
services/document-updater/Makefile

@@ -86,7 +86,6 @@ test: format lint typecheck shellcheck test_unit test_acceptance
 test_unit:
 ifneq (,$(wildcard test/unit))
 	$(DOCKER_COMPOSE_TEST_UNIT) run --rm test_unit
-	$(MAKE) test_unit_clean
 endif
 
 test_clean: test_unit_clean

+ 7 - 3
services/document-updater/app/js/RedisManager.js

@@ -1,7 +1,5 @@
 const Settings = require('@overleaf/settings')
-const rclient = require('@overleaf/redis-wrapper').createClient(
-  Settings.redis.documentupdater
-)
+const RedisWrapper = require('@overleaf/redis-wrapper')
 const logger = require('@overleaf/logger')
 const OError = require('@overleaf/o-error')
 const { promisifyAll } = require('@overleaf/promise-utils')
@@ -11,6 +9,8 @@ const crypto = require('node:crypto')
 const async = require('async')
 const { docIsTooLarge } = require('./Limits')
 
+const rclient = RedisWrapper.createClient(Settings.redis.documentupdater)
+
 // Sometimes Redis calls take an unexpectedly long time.  We have to be
 // quick with Redis calls because we're holding a lock that expires
 // after 30 seconds. We can't let any errors in the rest of the stack
@@ -777,6 +777,10 @@ const RedisManager = {
     // binary in node < v5
     return crypto.createHash('sha1').update(docLines, 'utf8').digest('hex')
   },
+
+  async cleanupTestRedis() {
+    await RedisWrapper.cleanupTestRedis(rclient)
+  },
 }
 
 module.exports = RedisManager

+ 8 - 0
services/document-updater/app/js/mongodb.js

@@ -1,4 +1,7 @@
+// @ts-check
+
 const Metrics = require('@overleaf/metrics')
+const MongoUtils = require('@overleaf/mongo-utils')
 const Settings = require('@overleaf/settings')
 const { MongoClient, ObjectId } = require('mongodb-legacy')
 
@@ -20,9 +23,14 @@ async function healthCheck() {
 
 Metrics.mongodb.monitor(mongoClient)
 
+async function cleanupTestDatabase() {
+  await MongoUtils.cleanupTestDatabase(mongoClient)
+}
+
 module.exports = {
   db,
   ObjectId,
   mongoClient,
   healthCheck: require('node:util').callbackify(healthCheck),
+  cleanupTestDatabase,
 }

+ 19 - 6
services/document-updater/docker-compose.ci.yml

@@ -6,10 +6,23 @@ services:
   test_unit:
     image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
     user: node
+    volumes:
+      - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
+    entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: npm run test:unit:_run
     environment:
+      MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
+      REDIS_HOST: redis_test
+      QUEUES_REDIS_HOST: redis_test
+      HISTORY_REDIS_HOST: redis_test
+      ANALYTICS_QUEUES_REDIS_HOST: redis_test
+    depends_on:
+      mongo:
+        condition: service_started
+      redis_test:
+        condition: service_healthy
 
 
   test_acceptance:
@@ -17,10 +30,10 @@ services:
     image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
     environment:
       ELASTIC_SEARCH_DSN: es:9200
-      REDIS_HOST: redis
-      QUEUES_REDIS_HOST: redis
-      HISTORY_REDIS_HOST: redis
-      ANALYTICS_QUEUES_REDIS_HOST: redis
+      REDIS_HOST: redis_test
+      QUEUES_REDIS_HOST: redis_test
+      HISTORY_REDIS_HOST: redis_test
+      ANALYTICS_QUEUES_REDIS_HOST: redis_test
       MONGO_HOST: mongo
       POSTGRES_HOST: postgres
       MOCHA_GREP: ${MOCHA_GREP}
@@ -31,7 +44,7 @@ services:
     depends_on:
       mongo:
         condition: service_started
-      redis:
+      redis_test:
         condition: service_healthy
     user: node
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
@@ -45,7 +58,7 @@ services:
       - ./:/tmp/build/
     command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs .
     user: root
-  redis:
+  redis_test:
     image: redis:7.4.3
     healthcheck:
       test: ping="$$(redis-cli ping)" && [ "$$ping" = 'PONG' ]

+ 18 - 6
services/document-updater/docker-compose.yml

@@ -9,14 +9,26 @@ services:
       - .:/overleaf/services/document-updater
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
     working_dir: /overleaf/services/document-updater
     environment:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
+      MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
+      REDIS_HOST: redis_test
+      HISTORY_REDIS_HOST: redis_test
+      QUEUES_REDIS_HOST: redis_test
+      ANALYTICS_QUEUES_REDIS_HOST: redis_test
+    entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: npm run --silent test:unit
     user: node
+    depends_on:
+      mongo:
+        condition: service_started
+      redis_test:
+        condition: service_healthy
 
   test_acceptance:
     image: node:22.18.0
@@ -28,10 +40,10 @@ services:
     working_dir: /overleaf/services/document-updater
     environment:
       ELASTIC_SEARCH_DSN: es:9200
-      REDIS_HOST: redis
-      HISTORY_REDIS_HOST: redis
-      QUEUES_REDIS_HOST: redis
-      ANALYTICS_QUEUES_REDIS_HOST: redis
+      REDIS_HOST: redis_test
+      HISTORY_REDIS_HOST: redis_test
+      QUEUES_REDIS_HOST: redis_test
+      ANALYTICS_QUEUES_REDIS_HOST: redis_test
       MONGO_HOST: mongo
       POSTGRES_HOST: postgres
       MOCHA_GREP: ${MOCHA_GREP}
@@ -42,12 +54,12 @@ services:
     depends_on:
       mongo:
         condition: service_started
-      redis:
+      redis_test:
         condition: service_healthy
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: npm run --silent test:acceptance
 
-  redis:
+  redis_test:
     image: redis:7.4.3
     healthcheck:
       test: ping=$$(redis-cli ping) && [ "$$ping" = 'PONG' ]

+ 2 - 1
services/document-updater/package.json

@@ -7,7 +7,7 @@
     "start": "node app.js",
     "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
     "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
-    "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js",
+    "test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js",
     "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
     "nodemon": "node --watch app.js",
     "benchmark:apply": "node benchmarks/apply",
@@ -20,6 +20,7 @@
   "dependencies": {
     "@overleaf/logger": "*",
     "@overleaf/metrics": "*",
+    "@overleaf/mongo-utils": "*",
     "@overleaf/o-error": "*",
     "@overleaf/promise-utils": "*",
     "@overleaf/ranges-tracker": "*",

+ 0 - 1
services/filestore/Makefile

@@ -87,7 +87,6 @@ test: format lint typecheck shellcheck test_unit test_acceptance
 test_unit:
 ifneq (,$(wildcard test/unit))
 	$(DOCKER_COMPOSE_TEST_UNIT) run --rm test_unit
-	$(MAKE) test_unit_clean
 endif
 
 test_clean: test_unit_clean

+ 1 - 0
services/filestore/docker-compose.ci.yml

@@ -11,6 +11,7 @@ services:
     user: node
     command: npm run test:unit:_run
     environment:
+      MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
 

+ 1 - 0
services/filestore/docker-compose.yml

@@ -19,6 +19,7 @@ services:
     environment:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
+      MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
     command: npm run --silent test:unit

+ 1 - 1
services/filestore/package.json

@@ -14,7 +14,7 @@
     "format": "prettier --list-different $PWD/'**/*.*js'",
     "format:fix": "prettier --write $PWD/'**/*.*js'",
     "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
-    "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js",
+    "test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js",
     "lint:fix": "eslint --fix .",
     "types:check": "tsc --noEmit"
   },

+ 0 - 1
services/history-v1/Makefile

@@ -86,7 +86,6 @@ test: format lint typecheck shellcheck test_unit test_acceptance
 test_unit:
 ifneq (,$(wildcard test/unit))
 	$(DOCKER_COMPOSE_TEST_UNIT) run --rm test_unit
-	$(MAKE) test_unit_clean
 endif
 
 test_clean: test_unit_clean

+ 19 - 6
services/history-v1/docker-compose.ci.yml

@@ -9,10 +9,23 @@ services:
   test_unit:
     image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
     user: node
+    volumes:
+      - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
+    entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: npm run test:unit:_run
     environment:
+      MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
+      REDIS_HOST: redis_test
+      QUEUES_REDIS_HOST: redis_test
+      HISTORY_REDIS_HOST: redis_test
+      ANALYTICS_QUEUES_REDIS_HOST: redis_test
+    depends_on:
+      mongo:
+        condition: service_started
+      redis_test:
+        condition: service_healthy
 
 
   test_acceptance:
@@ -20,10 +33,10 @@ services:
     image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
     environment:
       ELASTIC_SEARCH_DSN: es:9200
-      REDIS_HOST: redis
-      QUEUES_REDIS_HOST: redis
-      HISTORY_REDIS_HOST: redis
-      ANALYTICS_QUEUES_REDIS_HOST: redis
+      REDIS_HOST: redis_test
+      QUEUES_REDIS_HOST: redis_test
+      HISTORY_REDIS_HOST: redis_test
+      ANALYTICS_QUEUES_REDIS_HOST: redis_test
       MONGO_HOST: mongo
       POSTGRES_HOST: postgres
       AWS_S3_ENDPOINT: https://minio:9000
@@ -44,7 +57,7 @@ services:
     depends_on:
       mongo:
         condition: service_started
-      redis:
+      redis_test:
         condition: service_healthy
       postgres:
         condition: service_healthy
@@ -68,7 +81,7 @@ services:
       - ./:/tmp/build/
     command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs .
     user: root
-  redis:
+  redis_test:
     image: redis:7.4.3
     healthcheck:
       test: ping="$$(redis-cli ping)" && [ "$$ping" = 'PONG' ]

+ 18 - 6
services/history-v1/docker-compose.yml

@@ -15,14 +15,26 @@ services:
       - .:/overleaf/services/history-v1
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
     working_dir: /overleaf/services/history-v1
     environment:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
+      MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
+      REDIS_HOST: redis_test
+      HISTORY_REDIS_HOST: redis_test
+      QUEUES_REDIS_HOST: redis_test
+      ANALYTICS_QUEUES_REDIS_HOST: redis_test
+    entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: npm run --silent test:unit
     user: node
+    depends_on:
+      mongo:
+        condition: service_started
+      redis_test:
+        condition: service_healthy
 
   test_acceptance:
     build:
@@ -38,10 +50,10 @@ services:
     working_dir: /overleaf/services/history-v1
     environment:
       ELASTIC_SEARCH_DSN: es:9200
-      REDIS_HOST: redis
-      HISTORY_REDIS_HOST: redis
-      QUEUES_REDIS_HOST: redis
-      ANALYTICS_QUEUES_REDIS_HOST: redis
+      REDIS_HOST: redis_test
+      HISTORY_REDIS_HOST: redis_test
+      QUEUES_REDIS_HOST: redis_test
+      ANALYTICS_QUEUES_REDIS_HOST: redis_test
       MONGO_HOST: mongo
       POSTGRES_HOST: postgres
       AWS_S3_ENDPOINT: https://minio:9000
@@ -61,7 +73,7 @@ services:
     depends_on:
       mongo:
         condition: service_started
-      redis:
+      redis_test:
         condition: service_healthy
       postgres:
         condition: service_healthy
@@ -76,7 +88,7 @@ services:
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: npm run --silent test:acceptance
 
-  redis:
+  redis_test:
     image: redis:7.4.3
     healthcheck:
       test: ping=$$(redis-cli ping) && [ "$$ping" = 'PONG' ]

+ 1 - 1
services/history-v1/package.json

@@ -66,7 +66,7 @@
     "format:fix": "prettier --write $PWD/'**/*.*js'",
     "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
     "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
-    "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js",
+    "test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js",
     "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
     "nodemon": "node --watch app.js",
     "migrate": "knex migrate:latest",

+ 8 - 0
services/history-v1/storage/lib/mongodb.js

@@ -1,4 +1,7 @@
+// @ts-check
+
 const Metrics = require('@overleaf/metrics')
+const MongoUtils = require('@overleaf/mongo-utils')
 
 const config = require('config')
 const { MongoClient } = require('mongodb')
@@ -18,6 +21,10 @@ const backedUpBlobs = db.collection('projectHistoryBackedUpBlobs')
 
 Metrics.mongodb.monitor(client)
 
+async function cleanupTestDatabase() {
+  await MongoUtils.cleanupTestDatabase(client)
+}
+
 module.exports = {
   client,
   db,
@@ -27,4 +34,5 @@ module.exports = {
   projects,
   shardedBlobs,
   backedUpBlobs,
+  cleanupTestDatabase,
 }

+ 7 - 0
services/history-v1/storage/lib/redis.js

@@ -1,3 +1,5 @@
+// @ts-check
+
 const config = require('config')
 const redis = require('@overleaf/redis-wrapper')
 
@@ -11,9 +13,14 @@ async function disconnect() {
   await Promise.all([rclientHistory.disconnect(), rclientLock.disconnect()])
 }
 
+async function cleanupTestRedis() {
+  await redis.cleanupTestRedis(rclientHistory)
+}
+
 module.exports = {
   rclientHistory,
   rclientLock,
   redis,
   disconnect,
+  cleanupTestRedis,
 }

+ 0 - 1
services/notifications/Makefile

@@ -86,7 +86,6 @@ test: format lint typecheck shellcheck test_unit test_acceptance
 test_unit:
 ifneq (,$(wildcard test/unit))
 	$(DOCKER_COMPOSE_TEST_UNIT) run --rm test_unit
-	$(MAKE) test_unit_clean
 endif
 
 test_clean: test_unit_clean

+ 8 - 0
services/notifications/app/js/mongodb.js

@@ -1,4 +1,7 @@
+// @ts-check
+
 const Metrics = require('@overleaf/metrics')
+const MongoUtils = require('@overleaf/mongo-utils')
 const Settings = require('@overleaf/settings')
 const { MongoClient, ObjectId } = require('mongodb-legacy')
 
@@ -11,8 +14,13 @@ const db = {
 
 Metrics.mongodb.monitor(mongoClient)
 
+async function cleanupTestDatabase() {
+  await MongoUtils.cleanupTestDatabase(mongoClient)
+}
+
 module.exports = {
   db,
   mongoClient,
   ObjectId,
+  cleanupTestDatabase,
 }

+ 7 - 0
services/notifications/docker-compose.ci.yml

@@ -6,10 +6,17 @@ services:
   test_unit:
     image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
     user: node
+    volumes:
+      - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
+    entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: npm run test:unit:_run
     environment:
+      MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
+    depends_on:
+      mongo:
+        condition: service_started
 
 
   test_acceptance:

+ 6 - 0
services/notifications/docker-compose.yml

@@ -9,14 +9,20 @@ services:
       - .:/overleaf/services/notifications
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
     working_dir: /overleaf/services/notifications
     environment:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
+      MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
+    entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: npm run --silent test:unit
     user: node
+    depends_on:
+      mongo:
+        condition: service_started
 
   test_acceptance:
     image: node:22.18.0

+ 2 - 1
services/notifications/package.json

@@ -7,7 +7,7 @@
     "start": "node app.js",
     "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
     "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
-    "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js",
+    "test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js",
     "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
     "nodemon": "node --watch app.js",
     "lint": "eslint --max-warnings 0 --format unix .",
@@ -21,6 +21,7 @@
   "dependencies": {
     "@overleaf/logger": "*",
     "@overleaf/metrics": "*",
+    "@overleaf/mongo-utils": "*",
     "@overleaf/settings": "*",
     "async": "^3.2.5",
     "body-parser": "^1.20.3",

+ 0 - 1
services/project-history/Makefile

@@ -86,7 +86,6 @@ test: format lint typecheck shellcheck test_unit test_acceptance
 test_unit:
 ifneq (,$(wildcard test/unit))
 	$(DOCKER_COMPOSE_TEST_UNIT) run --rm test_unit
-	$(MAKE) test_unit_clean
 endif
 
 test_clean: test_unit_clean

+ 5 - 0
services/project-history/app/js/RedisManager.js

@@ -365,6 +365,10 @@ async function clearCachedHistoryId(projectId) {
   await rclient.del(key)
 }
 
+async function cleanupTestRedis() {
+  await redis.cleanupTestRedis(rclient)
+}
+
 // EXPORTS
 
 const countUnprocessedUpdatesCb = callbackify(countUnprocessedUpdates)
@@ -442,4 +446,5 @@ export const promises = {
   getCachedHistoryId,
   setCachedHistoryId,
   clearCachedHistoryId,
+  cleanupTestRedis,
 }

+ 7 - 1
services/project-history/app/js/mongodb.js

@@ -1,10 +1,11 @@
 import Metrics from '@overleaf/metrics'
+import MongoUtils from '@overleaf/mongo-utils'
 import Settings from '@overleaf/settings'
 import mongodb from 'mongodb-legacy'
 const { MongoClient, ObjectId } = mongodb
 
 /**
- * @import { ProjectHistoryFailure } from './mongo-types.ts'
+ * @import { ProjectHistoryFailure } from './mongo-types'
  */
 
 export { ObjectId }
@@ -17,6 +18,10 @@ const mongoDb = mongoClient.db()
 
 Metrics.mongodb.monitor(mongoClient)
 
+async function cleanupTestDatabase() {
+  await MongoUtils.cleanupTestDatabase(mongoClient)
+}
+
 export const db = {
   deletedProjects: mongoDb.collection('deletedProjects'),
   projects: mongoDb.collection('projects'),
@@ -24,4 +29,5 @@ export const db = {
   projectHistoryFailures: mongoDb.collection('projectHistoryFailures'),
   projectHistoryLabels: mongoDb.collection('projectHistoryLabels'),
   projectHistorySyncState: mongoDb.collection('projectHistorySyncState'),
+  cleanupTestDatabase,
 }

+ 19 - 6
services/project-history/docker-compose.ci.yml

@@ -6,10 +6,23 @@ services:
   test_unit:
     image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
     user: node
+    volumes:
+      - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
+    entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: npm run test:unit:_run
     environment:
+      MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
+      REDIS_HOST: redis_test
+      QUEUES_REDIS_HOST: redis_test
+      HISTORY_REDIS_HOST: redis_test
+      ANALYTICS_QUEUES_REDIS_HOST: redis_test
+    depends_on:
+      mongo:
+        condition: service_started
+      redis_test:
+        condition: service_healthy
 
 
   test_acceptance:
@@ -17,10 +30,10 @@ services:
     image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
     environment:
       ELASTIC_SEARCH_DSN: es:9200
-      REDIS_HOST: redis
-      QUEUES_REDIS_HOST: redis
-      HISTORY_REDIS_HOST: redis
-      ANALYTICS_QUEUES_REDIS_HOST: redis
+      REDIS_HOST: redis_test
+      QUEUES_REDIS_HOST: redis_test
+      HISTORY_REDIS_HOST: redis_test
+      ANALYTICS_QUEUES_REDIS_HOST: redis_test
       MONGO_HOST: mongo
       POSTGRES_HOST: postgres
       MOCHA_GREP: ${MOCHA_GREP}
@@ -31,7 +44,7 @@ services:
     depends_on:
       mongo:
         condition: service_started
-      redis:
+      redis_test:
         condition: service_healthy
     user: node
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
@@ -45,7 +58,7 @@ services:
       - ./:/tmp/build/
     command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs .
     user: root
-  redis:
+  redis_test:
     image: redis:7.4.3
     healthcheck:
       test: ping="$$(redis-cli ping)" && [ "$$ping" = 'PONG' ]

+ 18 - 6
services/project-history/docker-compose.yml

@@ -9,14 +9,26 @@ services:
       - .:/overleaf/services/project-history
       - ../../node_modules:/overleaf/node_modules
       - ../../libraries:/overleaf/libraries
+      - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
     working_dir: /overleaf/services/project-history
     environment:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
+      MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
+      REDIS_HOST: redis_test
+      HISTORY_REDIS_HOST: redis_test
+      QUEUES_REDIS_HOST: redis_test
+      ANALYTICS_QUEUES_REDIS_HOST: redis_test
+    entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: npm run --silent test:unit
     user: node
+    depends_on:
+      mongo:
+        condition: service_started
+      redis_test:
+        condition: service_healthy
 
   test_acceptance:
     image: node:22.18.0
@@ -28,10 +40,10 @@ services:
     working_dir: /overleaf/services/project-history
     environment:
       ELASTIC_SEARCH_DSN: es:9200
-      REDIS_HOST: redis
-      HISTORY_REDIS_HOST: redis
-      QUEUES_REDIS_HOST: redis
-      ANALYTICS_QUEUES_REDIS_HOST: redis
+      REDIS_HOST: redis_test
+      HISTORY_REDIS_HOST: redis_test
+      QUEUES_REDIS_HOST: redis_test
+      ANALYTICS_QUEUES_REDIS_HOST: redis_test
       MONGO_HOST: mongo
       POSTGRES_HOST: postgres
       MOCHA_GREP: ${MOCHA_GREP}
@@ -42,12 +54,12 @@ services:
     depends_on:
       mongo:
         condition: service_started
-      redis:
+      redis_test:
         condition: service_healthy
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: npm run --silent test:acceptance
 
-  redis:
+  redis_test:
     image: redis:7.4.3
     healthcheck:
       test: ping=$$(redis-cli ping) && [ "$$ping" = 'PONG' ]

+ 2 - 1
services/project-history/package.json

@@ -10,7 +10,7 @@
     "start": "node app.js",
     "nodemon": "node --watch app.js",
     "test:acceptance:_run": "mocha --loader=esmock --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
-    "test:unit:_run": "mocha --loader=esmock --recursive --reporter spec $@ test/unit/js",
+    "test:unit:_run": "mocha --loader=esmock --recursive --reporter spec --exit $@ test/unit/js",
     "lint": "eslint --max-warnings 0 --format unix .",
     "format": "prettier --list-different $PWD/'**/*.*js'",
     "format:fix": "prettier --write $PWD/'**/*.*js'",
@@ -21,6 +21,7 @@
     "@overleaf/fetch-utils": "*",
     "@overleaf/logger": "*",
     "@overleaf/metrics": "*",
+    "@overleaf/mongo-utils": "*",
     "@overleaf/o-error": "*",
     "@overleaf/promise-utils": "*",
     "@overleaf/redis-wrapper": "*",

+ 0 - 1
services/real-time/Makefile

@@ -86,7 +86,6 @@ test: format lint typecheck shellcheck test_unit test_acceptance
 test_unit:
 ifneq (,$(wildcard test/unit))
 	$(DOCKER_COMPOSE_TEST_UNIT) run --rm test_unit
-	$(MAKE) test_unit_clean
 endif
 
 test_clean: test_unit_clean

+ 14 - 6
services/real-time/docker-compose.ci.yml

@@ -8,8 +8,16 @@ services:
     user: node
     command: npm run test:unit:_run
     environment:
+      MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
+      REDIS_HOST: redis_test
+      QUEUES_REDIS_HOST: redis_test
+      HISTORY_REDIS_HOST: redis_test
+      ANALYTICS_QUEUES_REDIS_HOST: redis_test
+    depends_on:
+      redis_test:
+        condition: service_healthy
 
 
   test_acceptance:
@@ -17,17 +25,17 @@ services:
     image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
     environment:
       ELASTIC_SEARCH_DSN: es:9200
-      REDIS_HOST: redis
-      QUEUES_REDIS_HOST: redis
-      HISTORY_REDIS_HOST: redis
-      ANALYTICS_QUEUES_REDIS_HOST: redis
+      REDIS_HOST: redis_test
+      QUEUES_REDIS_HOST: redis_test
+      HISTORY_REDIS_HOST: redis_test
+      ANALYTICS_QUEUES_REDIS_HOST: redis_test
       MONGO_HOST: mongo
       POSTGRES_HOST: postgres
       MOCHA_GREP: ${MOCHA_GREP}
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
     depends_on:
-      redis:
+      redis_test:
         condition: service_healthy
     user: node
     command: npm run test:acceptance
@@ -40,7 +48,7 @@ services:
       - ./:/tmp/build/
     command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs .
     user: root
-  redis:
+  redis_test:
     image: redis:7.4.3
     healthcheck:
       test: ping="$$(redis-cli ping)" && [ "$$ping" = 'PONG' ]

+ 14 - 6
services/real-time/docker-compose.yml

@@ -13,10 +13,18 @@ services:
     environment:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
+      MONGO_CONNECTION_STRING: mongodb://mongo/test-overleaf
       NODE_ENV: test
       NODE_OPTIONS: "--unhandled-rejections=strict"
+      REDIS_HOST: redis_test
+      HISTORY_REDIS_HOST: redis_test
+      QUEUES_REDIS_HOST: redis_test
+      ANALYTICS_QUEUES_REDIS_HOST: redis_test
     command: npm run --silent test:unit
     user: node
+    depends_on:
+      redis_test:
+        condition: service_healthy
 
   test_acceptance:
     image: node:22.18.0
@@ -27,10 +35,10 @@ services:
     working_dir: /overleaf/services/real-time
     environment:
       ELASTIC_SEARCH_DSN: es:9200
-      REDIS_HOST: redis
-      HISTORY_REDIS_HOST: redis
-      QUEUES_REDIS_HOST: redis
-      ANALYTICS_QUEUES_REDIS_HOST: redis
+      REDIS_HOST: redis_test
+      HISTORY_REDIS_HOST: redis_test
+      QUEUES_REDIS_HOST: redis_test
+      ANALYTICS_QUEUES_REDIS_HOST: redis_test
       MONGO_HOST: mongo
       POSTGRES_HOST: postgres
       MOCHA_GREP: ${MOCHA_GREP}
@@ -39,11 +47,11 @@ services:
       NODE_OPTIONS: "--unhandled-rejections=strict"
     user: node
     depends_on:
-      redis:
+      redis_test:
         condition: service_healthy
     command: npm run --silent test:acceptance
 
-  redis:
+  redis_test:
     image: redis:7.4.3
     healthcheck:
       test: ping=$$(redis-cli ping) && [ "$$ping" = 'PONG' ]

+ 1 - 1
services/real-time/package.json

@@ -7,7 +7,7 @@
     "start": "node app.js",
     "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
     "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
-    "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js",
+    "test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js",
     "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
     "nodemon": "node --watch app.js",
     "lint": "eslint --max-warnings 0 --format unix .",

+ 1 - 12
services/web/app/src/infrastructure/RedisWrapper.js

@@ -20,18 +20,7 @@ function client(feature) {
 
 async function cleanupTestRedis() {
   const rclient = client()
-  ensureTestRedis(rclient)
-  await rclient.flushall()
-}
-
-function ensureTestRedis(rclient) {
-  const host = rclient.options.host
-  const env = process.env.NODE_ENV
-  if (host !== 'redis_test' || env !== 'test') {
-    throw new Error(
-      `Refusing to clear Redis instance '${host}' in environment '${env}'`
-    )
-  }
+  await redis.cleanupTestRedis(rclient)
 }
 
 module.exports = { client, cleanupTestRedis }

+ 3 - 23
services/web/app/src/infrastructure/mongodb.js

@@ -1,6 +1,7 @@
 const mongodb = require('mongodb-legacy')
 const OError = require('@overleaf/o-error')
 const Settings = require('@overleaf/settings')
+const MongoUtils = require('@overleaf/mongo-utils')
 const Mongoose = require('./Mongoose')
 const { addConnectionDrainer } = require('./GracefulShutdown')
 
@@ -93,32 +94,11 @@ async function getCollectionNames() {
 }
 
 async function cleanupTestDatabase() {
-  ensureTestDatabase()
-  const collectionNames = await getCollectionNames()
-  const collections = []
-  for (const name of collectionNames) {
-    if (name in db && name !== 'migrations') {
-      collections.push(db[name])
-    }
-  }
-  await Promise.all(collections.map(coll => coll.deleteMany({})))
+  await MongoUtils.cleanupTestDatabase(mongoClient)
 }
 
 async function dropTestDatabase() {
-  ensureTestDatabase()
-  await mongoClient.db().dropDatabase()
-}
-
-function ensureTestDatabase() {
-  const internalDb = mongoClient.db()
-  const dbName = internalDb.databaseName
-  const env = process.env.NODE_ENV
-
-  if (dbName !== 'test-overleaf' || env !== 'test') {
-    throw new OError(
-      `Refusing to clear database '${dbName}' in environment '${env}'`
-    )
-  }
+  await MongoUtils.dropTestDatabase(mongoClient)
 }
 
 /**