Switch to using vitest for tests

GitOrigin-RevId: 0e448010848011883f8843b6d3280e767cea932e
This commit is contained in:
Andrew Rumble
2025-07-08 09:36:24 +01:00
committed by Copybot
parent b173937d11
commit 579483588a
8 changed files with 978 additions and 1492 deletions

1878
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@ services:
user: node
volumes:
- ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
- ../../tsconfig.backend.json:/overleaf/tsconfig.backend.json
entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
command: npm run test:unit:_run
environment:

View File

@@ -9,6 +9,7 @@ services:
- .:/overleaf/services/notifications
- ../../node_modules:/overleaf/node_modules
- ../../libraries:/overleaf/libraries
- ../../tsconfig.backend.json:/overleaf/tsconfig.backend.json
- ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
working_dir: /overleaf/services/notifications
environment:
@@ -61,4 +62,3 @@ services:
# replica set. This override is not needed when running the setup after
# starting up mongo.
- mongo:127.0.0.1

View File

@@ -8,9 +8,8 @@
"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 --exit $@ test/unit/js",
"test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
"nodemon": "node --watch app.js",
"test:unit:_run": "vitest ${NO_CACHE:+\"--no-cache\"} $@ test/unit/js",
"test:unit": "npm run test:unit:_run",
"lint": "eslint --max-warnings 0 --format unix .",
"format": "prettier --list-different $PWD/'**/*.*js'",
"format:fix": "prettier --write $PWD/'**/*.*js'",
@@ -24,6 +23,7 @@
"@overleaf/metrics": "*",
"@overleaf/mongo-utils": "*",
"@overleaf/settings": "*",
"@overleaf/promise-utils": "*",
"async": "^3.2.5",
"body-parser": "^1.20.3",
"bunyan": "^1.8.15",
@@ -33,11 +33,9 @@
"request": "^2.88.2"
},
"devDependencies": {
"chai": "^4.3.6",
"@types/request": "^2.48.12",
"chai-as-promised": "^7.1.1",
"mocha": "^11.1.0",
"sandboxed-module": "^2.0.4",
"sinon": "^9.2.4",
"typescript": "^5.0.4"
"typescript": "^5.0.4",
"vitest": "^3.2.4"
}
}

View File

@@ -1,30 +1,27 @@
const chai = require('chai')
const SandboxedModule = require('sandboxed-module')
import { afterEach, chai, vi } from 'vitest'
import mongodb from 'mongodb-legacy'
import chaiAsPromised from 'chai-as-promised'
// Chai configuration
chai.should()
chai.use(chaiAsPromised)
// ensure every ObjectId has the id string as a property for correct comparisons
require('mongodb-legacy').ObjectId.cacheHexString = true
mongodb.ObjectId.cacheHexString = true
// SandboxedModule configuration
SandboxedModule.configure({
requires: {
'@overleaf/logger': {
debug() {},
log() {},
info() {},
warn() {},
err() {},
error() {},
fatal() {},
},
'mongodb-legacy': require('mongodb-legacy'), // for ObjectId comparisons
},
globals: { Buffer, JSON, console, process },
sourceTransformers: {
removeNodePrefix: function (source) {
return source.replace(/require\(['"]node:/g, "require('")
},
vi.mock('@overleaf/logger', () => ({
default: {
debug: vi.fn(),
log: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
err: vi.fn(),
error: vi.fn(),
fatal: vi.fn(),
},
}))
afterEach(() => {
vi.restoreAllMocks()
vi.resetModules()
})

View File

@@ -1,302 +1,239 @@
/* eslint-disable
no-dupe-keys,
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import { stub, assert as _assert } from 'sinon'
import { expect } from 'chai'
import { require as _require } from 'sandboxed-module'
import { deepEqual } from 'node:assert'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ObjectId } from 'mongodb-legacy'
import assert from 'node:assert'
const modulePath = '../../../app/js/Notifications.js'
const userId = '51dc93e6fb625a261300003b'
const notificationId = '574ee8d6f40c3a244e704249'
const notificationKey = 'notification-key'
describe('Notifications Tests', function () {
beforeEach(function () {
this.findToArrayStub = stub()
this.findStub = stub().returns({ toArray: this.findToArrayStub })
this.countStub = stub()
this.updateOneStub = stub()
this.deleteOneStub = stub()
this.db = {
describe('Notifications Tests', () => {
let countStub,
db,
deleteOneStub,
findStub,
findToArrayStub,
notifications,
stubbedNotification,
stubbedNotificationArray,
updateOneStub
beforeEach(async () => {
findToArrayStub = vi.fn()
findStub = vi.fn().mockReturnValue({ toArray: findToArrayStub })
countStub = vi.fn()
updateOneStub = vi.fn()
deleteOneStub = vi.fn()
db = {
notifications: {
find: this.findStub,
count: this.countStub,
updateOne: this.updateOneStub,
deleteOne: this.deleteOneStub,
find: findStub,
count: countStub,
updateOne: updateOneStub,
deleteOne: deleteOneStub,
},
}
this.notifications = _require(modulePath, {
requires: {
'@overleaf/settings': {},
'./mongodb': { db: this.db, ObjectId },
},
})
vi.doMock('@overleaf/settings', () => ({}))
vi.doMock('../../../app/js/mongodb', () => ({
db,
ObjectId,
}))
this.stubbedNotification = {
notifications = await import(modulePath)
stubbedNotification = {
user_id: new ObjectId(userId),
key: 'notification-key',
messageOpts: 'some info',
templateKey: 'template-key',
}
return (this.stubbedNotificationArray = [this.stubbedNotification])
stubbedNotificationArray = [stubbedNotification]
})
describe('getUserNotifications', function () {
return it('should find all notifications and return i', function (done) {
this.findToArrayStub.callsArgWith(0, null, this.stubbedNotificationArray)
return this.notifications.getUserNotifications(
userId,
(err, notifications) => {
if (err) return done(err)
notifications.should.equal(this.stubbedNotificationArray)
deepEqual(this.findStub.args[0][0], {
user_id: new ObjectId(userId),
templateKey: { $exists: true },
})
return done()
}
)
describe('getUserNotifications', () => {
it('should find all notifications and return i', async () => {
findToArrayStub.mockResolvedValue(stubbedNotificationArray)
const result = await notifications.getUserNotifications(userId)
result.should.equal(stubbedNotificationArray)
assert.deepEqual(findStub.mock.calls[0][0], {
user_id: new ObjectId(userId),
templateKey: { $exists: true },
})
})
})
describe('addNotification', function () {
beforeEach(function () {
this.stubbedNotification = {
describe('addNotification', () => {
let expectedDocument, expectedQuery
beforeEach(() => {
stubbedNotification = {
user_id: new ObjectId(userId),
key: 'notification-key',
messageOpts: 'some info',
templateKey: 'template-key',
}
this.expectedDocument = {
user_id: this.stubbedNotification.user_id,
expectedDocument = {
user_id: stubbedNotification.user_id,
key: 'notification-key',
messageOpts: 'some info',
templateKey: 'template-key',
}
this.expectedQuery = {
user_id: this.stubbedNotification.user_id,
expectedQuery = {
user_id: stubbedNotification.user_id,
key: 'notification-key',
}
this.updateOneStub.yields()
return this.countStub.yields(null, 0)
updateOneStub.mockResolvedValue()
countStub.mockResolvedValue(0)
})
it('should insert the notification into the collection', function (done) {
return this.notifications.addNotification(
userId,
this.stubbedNotification,
err => {
expect(err).not.to.exist
_assert.calledWith(
this.updateOneStub,
this.expectedQuery,
{ $set: this.expectedDocument },
{ upsert: true }
)
return done()
}
it('should insert the notification into the collection', async () => {
await notifications.addNotification(userId, stubbedNotification)
expect(updateOneStub).toHaveBeenCalledWith(
expectedQuery,
{ $set: expectedDocument },
{ upsert: true }
)
})
describe('when there is an existing notification', function (done) {
beforeEach(function () {
return this.countStub.yields(null, 1)
describe('when there is an existing notification', done => {
beforeEach(() => {
countStub.mockResolvedValue(1)
})
it('should fail to insert', function (done) {
return this.notifications.addNotification(
userId,
this.stubbedNotification,
err => {
expect(err).not.to.exist
_assert.notCalled(this.updateOneStub)
return done()
}
)
it('should fail to insert', async () => {
await notifications.addNotification(userId, stubbedNotification)
expect(updateOneStub).not.toHaveBeenCalled()
})
return it('should update the key if forceCreate is true', function (done) {
this.stubbedNotification.forceCreate = true
return this.notifications.addNotification(
userId,
this.stubbedNotification,
err => {
expect(err).not.to.exist
_assert.calledWith(
this.updateOneStub,
this.expectedQuery,
{ $set: this.expectedDocument },
{ upsert: true }
)
return done()
}
it('should update the key if forceCreate is true', async () => {
stubbedNotification.forceCreate = true
await notifications.addNotification(userId, stubbedNotification)
expect(updateOneStub).toHaveBeenCalledWith(
expectedQuery,
{ $set: expectedDocument },
{ upsert: true }
)
})
})
describe('when the notification is set to expire', function () {
beforeEach(function () {
this.stubbedNotification = {
describe('when the notification is set to expire', () => {
beforeEach(() => {
stubbedNotification = {
user_id: new ObjectId(userId),
key: 'notification-key',
messageOpts: 'some info',
templateKey: 'template-key',
expires: '2922-02-13T09:32:56.289Z',
}
this.expectedDocument = {
user_id: this.stubbedNotification.user_id,
expectedDocument = {
user_id: stubbedNotification.user_id,
key: 'notification-key',
messageOpts: 'some info',
templateKey: 'template-key',
expires: new Date(this.stubbedNotification.expires),
expires: new Date(stubbedNotification.expires),
}
return (this.expectedQuery = {
user_id: this.stubbedNotification.user_id,
expectedQuery = {
user_id: stubbedNotification.user_id,
key: 'notification-key',
})
}
})
return it('should add an `expires` Date field to the document', function (done) {
return this.notifications.addNotification(
userId,
this.stubbedNotification,
err => {
expect(err).not.to.exist
_assert.calledWith(
this.updateOneStub,
this.expectedQuery,
{ $set: this.expectedDocument },
{ upsert: true }
)
return done()
}
it('should add an `expires` Date field to the document', async () => {
await notifications.addNotification(userId, stubbedNotification)
expect(updateOneStub).toHaveBeenCalledWith(
expectedQuery,
{ $set: expectedDocument },
{ upsert: true }
)
})
})
return describe('when the notification has a nonsensical expires field', function () {
beforeEach(function () {
this.stubbedNotification = {
describe('when the notification has a nonsensical expires field', () => {
beforeEach(() => {
stubbedNotification = {
user_id: new ObjectId(userId),
key: 'notification-key',
messageOpts: 'some info',
templateKey: 'template-key',
expires: 'WAT',
}
return (this.expectedDocument = {
user_id: this.stubbedNotification.user_id,
expectedDocument = {
user_id: stubbedNotification.user_id,
key: 'notification-key',
messageOpts: 'some info',
templateKey: 'template-key',
expires: new Date(this.stubbedNotification.expires),
})
expires: new Date(stubbedNotification.expires),
}
})
return it('should produce an error', function (done) {
return this.notifications.addNotification(
userId,
this.stubbedNotification,
err => {
;(err instanceof Error).should.equal(true)
_assert.notCalled(this.updateOneStub)
return done()
}
)
it('should produce an error', async () => {
await expect(
notifications.addNotification(userId, stubbedNotification)
).to.eventually.be.rejectedWith(Error)
expect(updateOneStub).not.toHaveBeenCalled()
})
})
})
describe('removeNotificationId', function () {
return it('should mark the notification id as read', function (done) {
this.updateOneStub.callsArgWith(2, null)
describe('removeNotificationId', () => {
it('should mark the notification id as read', async () => {
updateOneStub.mockResolvedValue(null)
return this.notifications.removeNotificationId(
userId,
notificationId,
err => {
if (err) return done(err)
const searchOps = {
user_id: new ObjectId(userId),
_id: new ObjectId(notificationId),
}
const updateOperation = {
$unset: { templateKey: true, messageOpts: true },
}
deepEqual(this.updateOneStub.args[0][0], searchOps)
deepEqual(this.updateOneStub.args[0][1], updateOperation)
return done()
}
)
await notifications.removeNotificationId(userId, notificationId)
const searchOps = {
user_id: new ObjectId(userId),
_id: new ObjectId(notificationId),
}
const updateOperation = {
$unset: { templateKey: true, messageOpts: true },
}
assert.deepEqual(updateOneStub.mock.calls[0][0], searchOps)
assert.deepEqual(updateOneStub.mock.calls[0][1], updateOperation)
})
})
describe('removeNotificationKey', function () {
return it('should mark the notification key as read', function (done) {
this.updateOneStub.callsArgWith(2, null)
describe('removeNotificationKey', () => {
it('should mark the notification key as read', async () => {
updateOneStub.mockResolvedValue(null)
return this.notifications.removeNotificationKey(
userId,
notificationKey,
err => {
if (err) return done(err)
const searchOps = {
user_id: new ObjectId(userId),
key: notificationKey,
}
const updateOperation = {
$unset: { templateKey: true },
}
deepEqual(this.updateOneStub.args[0][0], searchOps)
deepEqual(this.updateOneStub.args[0][1], updateOperation)
return done()
}
)
await notifications.removeNotificationKey(userId, notificationKey)
const searchOps = {
user_id: new ObjectId(userId),
key: notificationKey,
}
const updateOperation = {
$unset: { templateKey: true },
}
assert.deepEqual(updateOneStub.mock.calls[0][0], searchOps)
assert.deepEqual(updateOneStub.mock.calls[0][1], updateOperation)
})
})
describe('removeNotificationByKeyOnly', function () {
return it('should mark the notification key as read', function (done) {
this.updateOneStub.callsArgWith(2, null)
describe('removeNotificationByKeyOnly', () => {
it('should mark the notification key as read', async () => {
updateOneStub.mockResolvedValue(null)
return this.notifications.removeNotificationByKeyOnly(
notificationKey,
err => {
if (err) return done(err)
const searchOps = { key: notificationKey }
const updateOperation = { $unset: { templateKey: true } }
deepEqual(this.updateOneStub.args[0][0], searchOps)
deepEqual(this.updateOneStub.args[0][1], updateOperation)
return done()
}
)
await notifications.removeNotificationByKeyOnly(notificationKey)
const searchOps = { key: notificationKey }
const updateOperation = { $unset: { templateKey: true } }
assert.deepEqual(updateOneStub.mock.calls[0][0], searchOps)
assert.deepEqual(updateOneStub.mock.calls[0][1], updateOperation)
})
})
return describe('deleteNotificationByKeyOnly', function () {
return it('should completely remove the notification', function (done) {
this.deleteOneStub.callsArgWith(1, null)
describe('deleteNotificationByKeyOnly', () => {
it('should completely remove the notification', async () => {
deleteOneStub.mockResolvedValue()
return this.notifications.deleteNotificationByKeyOnly(
notificationKey,
err => {
if (err) return done(err)
const searchOps = { key: notificationKey }
deepEqual(this.deleteOneStub.args[0][0], searchOps)
return done()
}
)
await notifications.deleteNotificationByKeyOnly(notificationKey)
const searchOps = { key: notificationKey }
expect(deleteOneStub.mock.calls[0][0]).toEqual(searchOps)
})
})
})

View File

@@ -1,7 +1,3 @@
/* eslint-disable
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
@@ -9,141 +5,157 @@
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import { stub } from 'sinon'
import { require as _require } from 'sandboxed-module'
import assert from 'node:assert'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const modulePath = '../../../app/js/NotificationsController.js'
const userId = '51dc93e6fb625a261300003b'
const notificationId = 'fb625a26f09d'
const notificationKey = 'my-notification-key'
describe('Notifications Controller', function () {
beforeEach(function () {
const self = this
this.notifications = {}
this.controller = _require(modulePath, {
requires: {
'./Notifications': this.notifications,
'@overleaf/metrics': {
inc: stub(),
},
},
})
describe('Notifications Controller', () => {
let controller, notifications, stubbedNotification
beforeEach(async () => {
notifications = {
addNotification: vi.fn(),
getUserNotifications: vi.fn(),
removeNotificationByKeyOnly: vi.fn(),
removeNotificationId: vi.fn(),
removeNotificationKey: vi.fn(),
}
return (this.stubbedNotification = [
vi.doMock('../../../app/js/Notifications', () => notifications)
vi.doMock('@overleaf/metrics', () => ({
default: {
inc: vi.fn(),
},
}))
controller = (await import(modulePath)).default
stubbedNotification = [
{
key: notificationKey,
messageOpts: 'some info',
templateKey: 'template-key',
},
])
]
})
describe('getUserNotifications', function () {
return it('should ask the notifications for the users notifications', function (done) {
this.notifications.getUserNotifications = stub().callsArgWith(
1,
null,
this.stubbedNotification
)
describe('getUserNotifications', () => {
it('should ask the notifications for the users notifications', async () => {
notifications.getUserNotifications.mockResolvedValue(stubbedNotification)
const req = {
params: {
user_id: userId,
},
}
return this.controller.getUserNotifications(req, {
json: result => {
result.should.equal(this.stubbedNotification)
this.notifications.getUserNotifications
.calledWith(userId)
.should.equal(true)
return done()
},
await new Promise(resolve => {
controller.getUserNotifications(req, {
json: result => {
expect(result).toBe(stubbedNotification)
expect(notifications.getUserNotifications).toHaveBeenCalledWith(
userId
)
resolve()
},
})
})
})
})
describe('addNotification', function () {
return it('should tell the notifications to add the notification for the user', function (done) {
this.notifications.addNotification = stub().callsArgWith(2)
describe('addNotification', () => {
it('should tell the notifications to add the notification for the user', async () => {
notifications.addNotification.mockResolvedValue()
const req = {
params: {
user_id: userId,
},
body: this.stubbedNotification,
body: stubbedNotification,
}
return this.controller.addNotification(req, {
sendStatus: code => {
this.notifications.addNotification
.calledWith(userId, this.stubbedNotification)
.should.equal(true)
code.should.equal(200)
return done()
},
await new Promise(resolve => {
controller.addNotification(req, {
sendStatus: code => {
expect(notifications.addNotification).toHaveBeenCalledWith(
userId,
stubbedNotification
)
expect(code).toBe(200)
resolve()
},
})
})
})
})
describe('removeNotificationId', function () {
return it('should tell the notifications to mark the notification Id as read', function (done) {
this.notifications.removeNotificationId = stub().callsArgWith(2)
describe('removeNotificationId', () => {
it('should tell the notifications to mark the notification Id as read', async () => {
notifications.removeNotificationId.mockResolvedValue()
const req = {
params: {
user_id: userId,
notification_id: notificationId,
},
}
return this.controller.removeNotificationId(req, {
sendStatus: code => {
this.notifications.removeNotificationId
.calledWith(userId, notificationId)
.should.equal(true)
code.should.equal(200)
return done()
},
await new Promise(resolve => {
controller.removeNotificationId(req, {
sendStatus: code => {
expect(notifications.removeNotificationId).toHaveBeenCalledWith(
userId,
notificationId
)
expect(code).toBe(200)
resolve()
},
})
})
})
})
describe('removeNotificationKey', function () {
return it('should tell the notifications to mark the notification Key as read', function (done) {
this.notifications.removeNotificationKey = stub().callsArgWith(2)
describe('removeNotificationKey', () => {
it('should tell the notifications to mark the notification Key as read', async () => {
notifications.removeNotificationKey.mockResolvedValue()
const req = {
params: {
user_id: userId,
},
body: { key: notificationKey },
}
return this.controller.removeNotificationKey(req, {
sendStatus: code => {
this.notifications.removeNotificationKey
.calledWith(userId, notificationKey)
.should.equal(true)
code.should.equal(200)
return done()
},
await new Promise(resolve => {
controller.removeNotificationKey(req, {
sendStatus: code => {
expect(notifications.removeNotificationKey).toHaveBeenCalledWith(
userId,
notificationKey
)
expect(code).toBe(200)
resolve()
},
})
})
})
})
return describe('removeNotificationByKeyOnly', function () {
return it('should tell the notifications to mark the notification Key as read', function (done) {
this.notifications.removeNotificationByKeyOnly = stub().callsArgWith(1)
describe('removeNotificationByKeyOnly', () => {
it('should tell the notifications to mark the notification Key as read', async () => {
notifications.removeNotificationByKeyOnly.mockResolvedValue()
const req = {
params: {
key: notificationKey,
},
}
return this.controller.removeNotificationByKeyOnly(req, {
sendStatus: code => {
this.notifications.removeNotificationByKeyOnly
.calledWith(notificationKey)
.should.equal(true)
code.should.equal(200)
return done()
},
})
await new Promise(resolve =>
controller.removeNotificationByKeyOnly(req, {
sendStatus: code => {
expect(
notifications.removeNotificationByKeyOnly
).toHaveBeenCalledWith(notificationKey)
expect(code).toBe(200)
resolve()
},
})
)
})
})
})

View File

@@ -2,6 +2,7 @@
"extends": "../../tsconfig.backend.json",
"include": [
"app.js",
"vitest.config.cjs",
"app/js/**/*",
"benchmarks/**/*",
"config/**/*",