diff --git a/package-lock.json b/package-lock.json index 1ad451a1a7..748c0e76c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16633,6 +16633,15 @@ "node": ">=6" } }, + "node_modules/esmock": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/esmock/-/esmock-2.1.0.tgz", + "integrity": "sha512-8/2+iFfcB5FMJDBWXmXCY/4GSaI8sMCWUmq2laroQc3y9AI53QMm5Ew25DkW9FMaM8dBH8hmvOr2l3qChJ2JgA==", + "dev": true, + "engines": { + "node": ">=14.16.0" + } + }, "node_modules/espree": { "version": "9.3.2", "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", @@ -36142,17 +36151,51 @@ "body-parser": "^1.19.0", "bunyan": "^1.8.15", "express": "^4.17.1", - "mongodb": "^3.6.0", + "mongodb": "^4.12.1", "request": "~2.88.2", "underscore": "~1.13.1" }, "devDependencies": { "chai": "^4.3.6", "chai-as-promised": "^7.1.1", + "esmock": "^2.1.0", "mocha": "^8.4.0", - "sandboxed-module": "~2.0.3", "sinon": "~9.0.1", - "timekeeper": "2.2.0" + "sinon-chai": "^3.7.0" + } + }, + "services/contacts/node_modules/bson": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.0.tgz", + "integrity": "sha512-VrlEE4vuiO1WTpfof4VmaVolCVYkYTgB9iWgYNOrVlnifpME/06fhFRmONgBhClD5pFC1t9ZWqFUQEQAzY43bA==", + "dependencies": { + "buffer": "^5.6.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "services/contacts/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" } }, "services/contacts/node_modules/diff": { @@ -36164,6 +36207,23 @@ "node": ">=0.3.1" } }, + "services/contacts/node_modules/mongodb": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.12.1.tgz", + "integrity": "sha512-koT87tecZmxPKtxRQD8hCKfn+ockEL2xBiUvx3isQGI6mFmagWt4f4AyCE9J4sKepnLhMacoCTQQA6SLAI2L6w==", + "dependencies": { + "bson": "^4.7.0", + "mongodb-connection-string-url": "^2.5.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">=12.9.0" + }, + "optionalDependencies": { + "@aws-sdk/credential-providers": "^3.186.0", + "saslprep": "^1.0.3" + } + }, "services/contacts/node_modules/sinon": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.0.3.tgz", @@ -46555,22 +46615,51 @@ "bunyan": "^1.8.15", "chai": "^4.3.6", "chai-as-promised": "^7.1.1", + "esmock": "^2.1.0", "express": "^4.17.1", "mocha": "^8.4.0", - "mongodb": "^3.6.0", + "mongodb": "^4.12.1", "request": "~2.88.2", - "sandboxed-module": "~2.0.3", "sinon": "~9.0.1", - "timekeeper": "2.2.0", + "sinon-chai": "^3.7.0", "underscore": "~1.13.1" }, "dependencies": { + "bson": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.0.tgz", + "integrity": "sha512-VrlEE4vuiO1WTpfof4VmaVolCVYkYTgB9iWgYNOrVlnifpME/06fhFRmONgBhClD5pFC1t9ZWqFUQEQAzY43bA==", + "requires": { + "buffer": "^5.6.0" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, + "mongodb": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.12.1.tgz", + "integrity": "sha512-koT87tecZmxPKtxRQD8hCKfn+ockEL2xBiUvx3isQGI6mFmagWt4f4AyCE9J4sKepnLhMacoCTQQA6SLAI2L6w==", + "requires": { + "@aws-sdk/credential-providers": "^3.186.0", + "bson": "^4.7.0", + "mongodb-connection-string-url": "^2.5.4", + "saslprep": "^1.0.3", + "socks": "^2.7.1" + } + }, "sinon": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.0.3.tgz", @@ -57545,6 +57634,12 @@ "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" }, + "esmock": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/esmock/-/esmock-2.1.0.tgz", + "integrity": "sha512-8/2+iFfcB5FMJDBWXmXCY/4GSaI8sMCWUmq2laroQc3y9AI53QMm5Ew25DkW9FMaM8dBH8hmvOr2l3qChJ2JgA==", + "dev": true + }, "espree": { "version": "9.3.2", "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", diff --git a/services/contacts/.eslintrc b/services/contacts/.eslintrc new file mode 100644 index 0000000000..cc68024d9d --- /dev/null +++ b/services/contacts/.eslintrc @@ -0,0 +1,6 @@ +{ + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module" + } +} diff --git a/services/contacts/app.js b/services/contacts/app.js index 7b5453a158..b7637ebc71 100644 --- a/services/contacts/app.js +++ b/services/contacts/app.js @@ -1,71 +1,21 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const Metrics = require('@overleaf/metrics') -Metrics.initialize('contacts') +import logger from '@overleaf/logger' +import Settings from '@overleaf/settings' +import { mongoClient } from './app/js/mongodb.js' +import { app } from './app/js/server.js' -const Settings = require('@overleaf/settings') -const logger = require('@overleaf/logger') -const express = require('express') -const bodyParser = require('body-parser') -const mongodb = require('./app/js/mongodb') -const Errors = require('./app/js/Errors') -const HttpController = require('./app/js/HttpController') +const { host, port } = Settings.internal.contacts -logger.initialize('contacts') -if (Metrics.event_loop != null) { - Metrics.event_loop.monitor(logger) +try { + await mongoClient.connect() +} catch (err) { + logger.fatal({ err }, 'Cannot connect to mongo. Exiting.') + process.exit(1) } -const app = express() - -app.use(Metrics.http.monitor(logger)) - -Metrics.injectMetricsRoute(app) - -app.get('/user/:user_id/contacts', HttpController.getContacts) -app.post( - '/user/:user_id/contacts', - bodyParser.json({ limit: '2mb' }), - HttpController.addContact -) - -app.get('/status', (req, res) => res.send('contacts is alive')) - -app.use(function (error, req, res, next) { - logger.error({ err: error }, 'request errored') - if (error instanceof Errors.NotFoundError) { - return res.sendStatus(404) - } else { - return res.status(500).send('Oops, something went wrong') +app.listen(port, host, err => { + if (err) { + logger.fatal({ err }, `Cannot bind to ${host}:${port}. Exiting.`) + process.exit(1) } + logger.debug(`contacts starting up, listening on ${host}:${port}`) }) - -const { port } = Settings.internal.contacts -const { host } = Settings.internal.contacts - -if (!module.parent) { - // Called directly - mongodb - .waitForDb() - .then(() => { - app.listen(port, host, function (err) { - if (err) { - logger.fatal({ err }, `Cannot bind to ${host}:${port}. Exiting.`) - process.exit(1) - } - return logger.debug( - `contacts starting up, listening on ${host}:${port}` - ) - }) - }) - .catch(err => { - logger.fatal({ err }, 'Cannot connect to mongo. Exiting.') - process.exit(1) - }) -} - -module.exports = app diff --git a/services/contacts/app/js/ContactManager.js b/services/contacts/app/js/ContactManager.js index b5810e4aaa..e8ab8f1386 100644 --- a/services/contacts/app/js/ContactManager.js +++ b/services/contacts/app/js/ContactManager.js @@ -1,76 +1,24 @@ -/* eslint-disable - camelcase, -*/ -// 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 - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -let ContactManager -const { db, ObjectId } = require('./mongodb') -const logger = require('@overleaf/logger') -const metrics = require('@overleaf/metrics') +import { db, ObjectId } from './mongodb.js' -module.exports = ContactManager = { - touchContact(user_id, contact_id, callback) { - if (callback == null) { - callback = function () {} - } - try { - user_id = ObjectId(user_id.toString()) - } catch (error1) { - const error = error1 - return callback(error) - } - - const update = { $set: {}, $inc: {} } - update.$inc[`contacts.${contact_id}.n`] = 1 - update.$set[`contacts.${contact_id}.ts`] = new Date() - - db.contacts.updateOne( - { - user_id, +export async function touchContact(userId, contactId) { + await db.contacts.updateOne( + { user_id: ObjectId(userId.toString()) }, + { + $inc: { + [`contacts.${contactId}.n`]: 1, }, - update, - { - upsert: true, + $set: { + [`contacts.${contactId}.ts`]: new Date(), }, - callback - ) - }, - - getContacts(user_id, callback) { - if (callback == null) { - callback = function () {} - } - try { - user_id = ObjectId(user_id.toString()) - } catch (error1) { - const error = error1 - return callback(error) - } - - return db.contacts.findOne( - { - user_id, - }, - function (error, user) { - if (error != null) { - return callback(error) - } - return callback(null, user != null ? user.contacts : undefined) - } - ) - }, -} -;['touchContact', 'getContacts'].map(method => - metrics.timeAsyncMethod( - ContactManager, - method, - 'mongo.ContactManager', - logger + }, + { upsert: true } ) -) +} + +export async function getContacts(userId) { + const user = await db.contacts.findOne({ + user_id: ObjectId(userId.toString()), + }) + + return user?.contacts +} diff --git a/services/contacts/app/js/Errors.js b/services/contacts/app/js/Errors.js index 21036bcaec..931579b5be 100644 --- a/services/contacts/app/js/Errors.js +++ b/services/contacts/app/js/Errors.js @@ -1,16 +1,6 @@ -/* eslint-disable - no-proto, - no-unused-vars, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -let Errors -function NotFoundError(message) { - const error = new Error(message) - error.name = 'NotFoundError' - error.__proto__ = NotFoundError.prototype - return error +export class NotFoundError extends Error { + constructor(message) { + super(message) + this.name = 'NotFoundError' + } } -NotFoundError.prototype.__proto__ = Error.prototype - -module.exports = Errors = { NotFoundError } diff --git a/services/contacts/app/js/HttpController.js b/services/contacts/app/js/HttpController.js index f1e65f59a5..be94292a50 100644 --- a/services/contacts/app/js/HttpController.js +++ b/services/contacts/app/js/HttpController.js @@ -1,100 +1,48 @@ -/* eslint-disable - camelcase, -*/ -// 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 - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -let HttpController -const ContactManager = require('./ContactManager') -const logger = require('@overleaf/logger') +import logger from '@overleaf/logger' +import * as ContactManager from './ContactManager.js' +import { buildContactIds } from './contacts.js' -module.exports = HttpController = { - addContact(req, res, next) { - const { user_id } = req.params - const { contact_id } = req.body +const CONTACT_LIMIT = 50 - if (contact_id == null || contact_id === '') { - res.status(400).send('contact_id should be a non-blank string') - return - } +export function addContact(req, res, next) { + const { user_id: userId } = req.params + const { contact_id: contactId } = req.body - logger.debug({ user_id, contact_id }, 'adding contact') + if (contactId == null || contactId === '') { + res.status(400).send('contact_id should be a non-blank string') + return + } - return ContactManager.touchContact(user_id, contact_id, function (error) { - if (error != null) { - return next(error) - } - return ContactManager.touchContact(contact_id, user_id, function (error) { - if (error != null) { - return next(error) - } - return res.sendStatus(204) - }) + logger.debug({ user_id: userId, contact_id: contactId }, 'adding contact') + + Promise.all([ + ContactManager.touchContact(userId, contactId), + ContactManager.touchContact(contactId, userId), + ]) + .then(() => { + res.sendStatus(204) }) - }, - - CONTACT_LIMIT: 50, - getContacts(req, res, next) { - let limit - let { user_id } = req.params - - if ((req.query != null ? req.query.limit : undefined) != null) { - limit = parseInt(req.query.limit, 10) - } else { - limit = HttpController.CONTACT_LIMIT - } - limit = Math.min(limit, HttpController.CONTACT_LIMIT) - - logger.debug({ user_id }, 'getting contacts') - - return ContactManager.getContacts(user_id, function (error, contact_dict) { - if (error != null) { - return next(error) - } - - let contacts = [] - const object = contact_dict || {} - for (user_id in object) { - const data = object[user_id] - contacts.push({ - user_id, - n: data.n, - ts: data.ts, - }) - } - - HttpController._sortContacts(contacts) - contacts = contacts.slice(0, limit) - const contact_ids = contacts.map(contact => contact.user_id) - - return res.status(200).send({ - contact_ids, - }) + .catch(error => { + next(error) + }) +} + +export function getContacts(req, res, next) { + const { user_id: userId } = req.params + const { limit } = req.query + + const contactLimit = + limit == null ? CONTACT_LIMIT : Math.min(parseInt(limit, 10), CONTACT_LIMIT) + + logger.debug({ user_id: userId }, 'getting contacts') + + ContactManager.getContacts(userId) + .then(contacts => { + res.json({ + contact_ids: buildContactIds(contacts, contactLimit), + }) + }) + .catch(error => { + next(error) }) - }, - - _sortContacts(contacts) { - return contacts.sort(function (a, b) { - // Sort by decreasing count, descreasing timestamp. - // I.e. biggest count, and most recent at front. - if (a.n > b.n) { - return -1 - } else if (a.n < b.n) { - return 1 - } else { - if (a.ts > b.ts) { - return -1 - } else if (a.ts < b.ts) { - return 1 - } else { - return 0 - } - } - }) - }, } diff --git a/services/contacts/app/js/contacts.js b/services/contacts/app/js/contacts.js new file mode 100644 index 0000000000..36a142c797 --- /dev/null +++ b/services/contacts/app/js/contacts.js @@ -0,0 +1,13 @@ +export function buildContactIds(contacts, limit) { + return Object.entries(contacts || {}) + .map(([id, { n, ts }]) => ({ id, n, ts })) + .sort(sortContacts) + .slice(0, limit) + .map(contact => contact.id) +} + +// sort by decreasing count, decreasing timestamp. +// i.e. highest count, most recent first. +function sortContacts(a, b) { + return a.n === b.n ? b.ts - a.ts : b.n - a.n +} diff --git a/services/contacts/app/js/mongodb.js b/services/contacts/app/js/mongodb.js index ecd9017283..93a3bfbd41 100644 --- a/services/contacts/app/js/mongodb.js +++ b/services/contacts/app/js/mongodb.js @@ -1,28 +1,12 @@ -const Settings = require('@overleaf/settings') -const { MongoClient, ObjectId } = require('mongodb') +import Settings from '@overleaf/settings' +import { MongoClient } from 'mongodb' -const clientPromise = MongoClient.connect( - Settings.mongo.url, - Settings.mongo.options -) +export { ObjectId } from 'mongodb' -let setupDbPromise -async function waitForDb() { - if (!setupDbPromise) { - setupDbPromise = setupDb() - } - await setupDbPromise -} - -const db = {} -async function setupDb() { - const internalDb = (await clientPromise).db() - - db.contacts = internalDb.collection('contacts') -} - -module.exports = { - db, - ObjectId, - waitForDb, +export const mongoClient = new MongoClient(Settings.mongo.url) + +const mongoDb = mongoClient.db() + +export const db = { + contacts: mongoDb.collection('contacts'), } diff --git a/services/contacts/app/js/server.js b/services/contacts/app/js/server.js new file mode 100644 index 0000000000..5fe888c1fe --- /dev/null +++ b/services/contacts/app/js/server.js @@ -0,0 +1,32 @@ +import * as Metrics from '@overleaf/metrics' +import logger from '@overleaf/logger' +import express from 'express' +import bodyParser from 'body-parser' +import * as HttpController from './HttpController.js' +import * as Errors from './Errors.js' + +Metrics.initialize('contacts') +logger.initialize('contacts') +Metrics.event_loop?.monitor(logger) + +export const app = express() +app.use(Metrics.http.monitor(logger)) +Metrics.injectMetricsRoute(app) + +app.get('/user/:user_id/contacts', HttpController.getContacts) +app.post( + '/user/:user_id/contacts', + bodyParser.json({ limit: '2mb' }), + HttpController.addContact +) + +app.get('/status', (req, res) => res.send('contacts is alive')) + +app.use(function (error, req, res, next) { + logger.error({ err: error }, 'request errored') + if (error instanceof Errors.NotFoundError) { + return res.sendStatus(404) + } else { + return res.status(500).send('Oops, something went wrong') + } +}) diff --git a/services/contacts/config/settings.defaults.js b/services/contacts/config/settings.defaults.cjs similarity index 74% rename from services/contacts/config/settings.defaults.js rename to services/contacts/config/settings.defaults.cjs index 516d362492..81833d4f84 100644 --- a/services/contacts/config/settings.defaults.js +++ b/services/contacts/config/settings.defaults.cjs @@ -10,10 +10,6 @@ module.exports = { }, mongo: { - options: { - useUnifiedTopology: - (process.env.MONGO_USE_UNIFIED_TOPOLOGY || 'true') === 'true', - }, url: process.env.MONGO_CONNECTION_STRING || `mongodb://${process.env.MONGO_HOST || 'localhost'}/sharelatex`, diff --git a/services/contacts/package.json b/services/contacts/package.json index 26d4ead781..f7dd6112b1 100644 --- a/services/contacts/package.json +++ b/services/contacts/package.json @@ -2,12 +2,13 @@ "name": "@overleaf/contacts", "description": "An API for tracking contacts of a user", "private": true, + "type": "module", "main": "app.js", "scripts": { "start": "node $NODE_APP_OPTIONS app.js", - "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js", + "test:acceptance:_run": "LOG_LEVEL=fatal 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 --recursive --reporter spec $@ test/unit/js", + "test:unit:_run": "LOG_LEVEL=fatal mocha --loader=esmock --recursive --reporter spec $@ test/unit/js", "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP", "nodemon": "nodemon --config nodemon.json", "lint": "eslint --max-warnings 0 --format unix .", @@ -23,16 +24,16 @@ "body-parser": "^1.19.0", "bunyan": "^1.8.15", "express": "^4.17.1", - "mongodb": "^3.6.0", + "mongodb": "^4.12.1", "request": "~2.88.2", "underscore": "~1.13.1" }, "devDependencies": { "chai": "^4.3.6", "chai-as-promised": "^7.1.1", + "esmock": "^2.1.0", "mocha": "^8.4.0", - "sandboxed-module": "~2.0.3", "sinon": "~9.0.1", - "timekeeper": "2.2.0" + "sinon-chai": "^3.7.0" } } diff --git a/services/contacts/test/acceptance/js/ContactsApp.js b/services/contacts/test/acceptance/js/ContactsApp.js deleted file mode 100644 index 161353ebf6..0000000000 --- a/services/contacts/test/acceptance/js/ContactsApp.js +++ /dev/null @@ -1,46 +0,0 @@ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * DS205: Consider reworking code to avoid use of IIFEs - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const app = require('../../../app') -const { waitForDb } = require('../../../app/js/mongodb') -require('@overleaf/logger').logger.level('error') - -module.exports = { - running: false, - initing: false, - callbacks: [], - ensureRunning(callback) { - if (callback == null) { - callback = function () {} - } - if (this.running) { - return callback() - } else if (this.initing) { - return this.callbacks.push(callback) - } - this.initing = true - this.callbacks.push(callback) - waitForDb().then(() => { - return app.listen(3036, 'localhost', error => { - if (error != null) { - throw error - } - this.running = true - return (() => { - const result = [] - for (callback of Array.from(this.callbacks)) { - result.push(callback()) - } - return result - })() - }) - }) - }, -} diff --git a/services/contacts/test/acceptance/js/GettingContactsTests.js b/services/contacts/test/acceptance/js/GettingContactsTests.js index 566acb0579..e166b3e98e 100644 --- a/services/contacts/test/acceptance/js/GettingContactsTests.js +++ b/services/contacts/test/acceptance/js/GettingContactsTests.js @@ -1,72 +1,71 @@ -/* eslint-disable - camelcase, - no-undef, - 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 - */ -const sinon = require('sinon') -const chai = require('chai') -chai.should() -const { expect } = chai -const { ObjectId } = require('mongodb') -const request = require('request') -const async = require('async') -const ContactsApp = require('./ContactsApp') +import { ObjectId } from 'mongodb' +import request from 'request' +import async from 'async' +import { app } from '../../../app/js/server.js' + const HOST = 'http://localhost:3036' describe('Getting Contacts', function () { + before(function (done) { + this.server = app.listen(3036, 'localhost', error => { + if (error != null) { + throw error + } + + done() + }) + }) + + after(function () { + this.server.close() + }) + describe('with no contacts', function () { - beforeEach(function (done) { + beforeEach(function () { this.user_id = ObjectId().toString() - return ContactsApp.ensureRunning(done) }) - return it('should return an empty array', function (done) { - return request( + it('should return an empty array', function (done) { + request( { method: 'GET', url: `${HOST}/user/${this.user_id}/contacts`, json: true, }, (error, response, body) => { - if (error) return done(error) + if (error) { + return done(error) + } response.statusCode.should.equal(200) body.contact_ids.should.deep.equal([]) - return done() + done() } ) }) }) - return describe('with contacts', function () { + describe('with contacts', function () { beforeEach(function (done) { this.user_id = ObjectId().toString() this.contact_id_1 = ObjectId().toString() this.contact_id_2 = ObjectId().toString() this.contact_id_3 = ObjectId().toString() - const touchContact = (user_id, contact_id, cb) => + const touchContact = (userId, contactId, cb) => request( { method: 'POST', - url: `${HOST}/user/${user_id}/contacts`, + url: `${HOST}/user/${userId}/contacts`, json: { - contact_id, + contact_id: contactId, }, }, cb ) - return async.series( + async.series( [ // 2 is preferred since touched twice, then 3 since most recent, then 1 - cb => ContactsApp.ensureRunning(cb), cb => touchContact(this.user_id, this.contact_id_1, cb), cb => touchContact(this.user_id, this.contact_id_2, cb), cb => touchContact(this.user_id, this.contact_id_2, cb), @@ -77,40 +76,44 @@ describe('Getting Contacts', function () { }) it('should return a sorted list of contacts', function (done) { - return request( + request( { method: 'GET', url: `${HOST}/user/${this.user_id}/contacts`, json: true, }, (error, response, body) => { - if (error) return done(error) + if (error) { + return done(error) + } response.statusCode.should.equal(200) body.contact_ids.should.deep.equal([ this.contact_id_2, this.contact_id_3, this.contact_id_1, ]) - return done() + done() } ) }) - return it('should respect a limit and only return top X contacts', function (done) { - return request( + it('should respect a limit and only return top X contacts', function (done) { + request( { method: 'GET', url: `${HOST}/user/${this.user_id}/contacts?limit=2`, json: true, }, (error, response, body) => { - if (error) return done(error) + if (error) { + return done(error) + } response.statusCode.should.equal(200) body.contact_ids.should.deep.equal([ this.contact_id_2, this.contact_id_3, ]) - return done() + done() } ) }) diff --git a/services/contacts/test/setup.js b/services/contacts/test/setup.js index cc542536eb..6b71d8f5ab 100644 --- a/services/contacts/test/setup.js +++ b/services/contacts/test/setup.js @@ -1,15 +1,7 @@ -const SandboxedModule = require('sandboxed-module') +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import sinonChai from 'sinon-chai' -SandboxedModule.configure({ - requires: { - '@overleaf/logger': { - debug() {}, - info() {}, - log() {}, - warn() {}, - error() {}, - }, - '@overleaf/metrics': { timeAsyncMethod() {} }, - }, - globals: { Buffer, console, process }, -}) +chai.should() +chai.use(chaiAsPromised) +chai.use(sinonChai) diff --git a/services/contacts/test/unit/js/ContactsManagerTests.js b/services/contacts/test/unit/js/ContactsManagerTests.js index 981471daaf..856ed48f9d 100644 --- a/services/contacts/test/unit/js/ContactsManagerTests.js +++ b/services/contacts/test/unit/js/ContactsManagerTests.js @@ -1,140 +1,97 @@ -/* eslint-disable - 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 - */ -const sinon = require('sinon') -const chai = require('chai') -const should = chai.should() -const { expect } = chai -const modulePath = '../../../app/js/ContactManager.js' -const SandboxedModule = require('sandboxed-module') -const { ObjectId } = require('mongodb') -const tk = require('timekeeper') +import sinon from 'sinon' +import { expect } from 'chai' +import esmock from 'esmock' +import { ObjectId } from 'mongodb' describe('ContactManager', function () { - beforeEach(function () { - tk.freeze(Date.now()) - this.ContactManager = SandboxedModule.require(modulePath, { - requires: { - './mongodb': { - db: (this.db = { contacts: {} }), - ObjectId, - }, + 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 = ObjectId().toString() this.contact_id = ObjectId().toString() - return (this.callback = sinon.stub()) }) afterEach(function () { - return tk.reset() + this.clock.restore() }) describe('touchContact', function () { beforeEach(function () { - this.db.contacts.updateOne = sinon.stub().callsArg(3) + this.db.contacts.updateOne = sinon.stub().resolves() }) describe('with a valid user_id', function () { - beforeEach(function () { - return this.ContactManager.touchContact( - this.user_id, - (this.contact_id = 'mock_contact'), - this.callback + 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(), + }, + }, + { + upsert: true, + } ) }) - - it('should increment the contact count and timestamp', function () { - this.db.contacts.updateOne - .calledWith( - { - user_id: sinon.match( - o => o.toString() === this.user_id.toString() - ), - }, - { - $inc: { - 'contacts.mock_contact.n': 1, - }, - $set: { - 'contacts.mock_contact.ts': new Date(), - }, - }, - { - upsert: true, - } - ) - .should.equal(true) - }) - - return it('should call the callback', function () { - return this.callback.called.should.equal(true) - }) }) - return describe('with an invalid user id', function () { - beforeEach(function () { - return this.ContactManager.touchContact( - 'not-valid-object-id', - this.contact_id, - this.callback + 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 + ) + ).to.be.rejectedWith( + 'Argument passed in must be a string of 12 bytes or a string of 24 hex characters or an integer' ) }) - - return it('should call the callback with an error', function () { - return this.callback.calledWith(sinon.match(Error)).should.equal(true) - }) }) }) - return describe('getContacts', function () { + describe('getContacts', function () { beforeEach(function () { this.user = { contacts: ['mock', 'contacts'], } - return (this.db.contacts.findOne = sinon - .stub() - .callsArgWith(1, null, this.user)) + this.db.contacts.findOne = sinon.stub().resolves(this.user) }) describe('with a valid user_id', function () { - beforeEach(function () { - return this.ContactManager.getContacts(this.user_id, this.callback) - }) + it("should find the user's contacts", async function () { + await expect( + this.ContactManager.getContacts(this.user_id) + ).to.eventually.deep.equal(this.user.contacts) - it("should find the user's contacts", function () { - return this.db.contacts.findOne - .calledWith({ - user_id: sinon.match(o => o.toString() === this.user_id.toString()), - }) - .should.equal(true) - }) - - return it('should call the callback with the contacts', function () { - return this.callback - .calledWith(null, this.user.contacts) - .should.equal(true) + expect(this.db.contacts.findOne).to.be.calledWith({ + user_id: sinon.match(o => o.toString() === this.user_id), + }) }) }) - return describe('with an invalid user id', function () { - beforeEach(function () { - return this.ContactManager.getContacts( - 'not-valid-object-id', - this.callback - ) - }) - - return it('should call the callback with an error', function () { - return this.callback.calledWith(sinon.match(Error)).should.equal(true) + 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 }) }) }) diff --git a/services/contacts/test/unit/js/HttpControllerTests.js b/services/contacts/test/unit/js/HttpControllerTests.js index 2bbb86b3ac..3a7bc15b42 100644 --- a/services/contacts/test/unit/js/HttpControllerTests.js +++ b/services/contacts/test/unit/js/HttpControllerTests.js @@ -1,29 +1,26 @@ -/* eslint-disable - mocha/no-pending-tests, - 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 - */ -const sinon = require('sinon') -const chai = require('chai') -const should = chai.should() -const { expect } = chai -const modulePath = '../../../app/js/HttpController.js' -const SandboxedModule = require('sandboxed-module') +import sinon from 'sinon' +import { expect } from 'chai' +import esmock from 'esmock' describe('HttpController', function () { - beforeEach(function () { - this.HttpController = SandboxedModule.require(modulePath, { - requires: { - './ContactManager': (this.ContactManager = {}), - }, + beforeEach(async function () { + const now = Date.now() + + this.contacts = { + 'user-id-1': { n: 2, ts: new Date(now) }, + 'user-id-2': { n: 4, ts: new Date(now) }, + 'user-id-3': { n: 2, ts: new Date(now - 1000) }, + } + + this.ContactManager = { + touchContact: sinon.stub().resolves(), + getContacts: sinon.stub().resolves(this.contacts), + } + + this.HttpController = await esmock('../../../app/js/HttpController', { + '../../../app/js/ContactManager': this.ContactManager, }) + this.user_id = 'mock-user-id' this.contact_id = 'mock-contact-id' @@ -31,118 +28,99 @@ describe('HttpController', function () { this.res = {} this.res.status = sinon.stub().returns(this.res) this.res.end = sinon.stub() + this.res.json = sinon.stub() this.res.send = sinon.stub() this.res.sendStatus = sinon.stub() - return (this.next = sinon.stub()) + this.next = sinon.stub() }) describe('addContact', function () { - beforeEach(function () { - this.req.params = { user_id: this.user_id } - return (this.ContactManager.touchContact = sinon.stub().callsArg(2)) - }) - describe('with a valid user_id and contact_id', function () { - beforeEach(function () { + beforeEach(async function () { + this.req.params = { user_id: this.user_id } this.req.body = { contact_id: this.contact_id } - return this.HttpController.addContact(this.req, this.res, this.next) + await this.HttpController.addContact(this.req, this.res, this.next) }) it("should update the contact in the user's contact list", function () { - return this.ContactManager.touchContact - .calledWith(this.user_id, this.contact_id) - .should.equal(true) + expect(this.ContactManager.touchContact).to.be.calledWith( + this.user_id, + this.contact_id + ) }) it("should update the user in the contact's contact list", function () { - return this.ContactManager.touchContact - .calledWith(this.contact_id, this.user_id) - .should.equal(true) + expect(this.ContactManager.touchContact).to.be.calledWith( + this.contact_id, + this.user_id + ) }) - return it('should send back a 204 status', function () { - this.res.sendStatus.calledWith(204).should.equal(true) + it('should send back a 204 status', function () { + expect(this.res.sendStatus).to.be.calledWith(204) }) }) - return describe('with an invalid contact id', function () { - beforeEach(function () { + describe('with an invalid contact id', function () { + beforeEach(async function () { + this.req.params = { user_id: this.user_id } this.req.body = { contact_id: '' } - return this.HttpController.addContact(this.req, this.res, this.next) + await this.HttpController.addContact(this.req, this.res, this.next) }) - return it('should return 400, Bad Request', function () { - this.res.status.calledWith(400).should.equal(true) - return this.res.send - .calledWith('contact_id should be a non-blank string') - .should.equal(true) + it('should return 400, Bad Request', function () { + expect(this.res.status).to.be.calledWith(400) + expect(this.res.send).to.be.calledWith( + 'contact_id should be a non-blank string' + ) }) }) }) - return describe('getContacts', function () { - beforeEach(function () { - this.req.params = { user_id: this.user_id } - const now = Date.now() - this.contacts = { - 'user-id-1': { n: 2, ts: new Date(now) }, - 'user-id-2': { n: 4, ts: new Date(now) }, - 'user-id-3': { n: 2, ts: new Date(now - 1000) }, - } - return (this.ContactManager.getContacts = sinon - .stub() - .callsArgWith(1, null, this.contacts)) - }) - + describe('getContacts', function () { describe('normally', function () { - beforeEach(function () { - return this.HttpController.getContacts(this.req, this.res, this.next) + beforeEach(async function () { + this.req.params = { user_id: this.user_id } + this.req.query = {} + await this.HttpController.getContacts(this.req, this.res, this.next) }) it('should look up the contacts in mongo', function () { - return this.ContactManager.getContacts - .calledWith(this.user_id) - .should.equal(true) + expect(this.ContactManager.getContacts).to.be.calledWith(this.user_id) }) - return it('should return a sorted list of contacts by count and timestamp', function () { - return this.res.send - .calledWith({ - contact_ids: ['user-id-2', 'user-id-1', 'user-id-3'], - }) - .should.equal(true) + it('should return a sorted list of contacts by count and timestamp', function () { + expect(this.res.json).to.be.calledWith({ + contact_ids: ['user-id-2', 'user-id-1', 'user-id-3'], + }) }) }) describe('with more contacts than the limit', function () { - beforeEach(function () { + beforeEach(async function () { + this.req.params = { user_id: this.user_id } this.req.query = { limit: 2 } - return this.HttpController.getContacts(this.req, this.res, this.next) + await this.HttpController.getContacts(this.req, this.res, this.next) }) - return it('should return the most commonly used contacts up to the limit', function () { - return this.res.send - .calledWith({ - contact_ids: ['user-id-2', 'user-id-1'], - }) - .should.equal(true) + it('should return the most commonly used contacts up to the limit', function () { + expect(this.res.json).to.be.calledWith({ + contact_ids: ['user-id-2', 'user-id-1'], + }) }) }) describe('without a contact list', function () { - beforeEach(function () { - this.ContactManager.getContacts = sinon - .stub() - .callsArgWith(1, null, null) - return this.HttpController.getContacts(this.req, this.res, this.next) + beforeEach(async function () { + this.ContactManager.getContacts.resolves(null) + + this.req.params = {} + this.req.query = {} + await this.HttpController.getContacts(this.req, this.res, this.next) }) - return it('should return an empty list', function () { - return this.res.send - .calledWith({ - contact_ids: [], - }) - .should.equal(true) + it('should return an empty list', function () { + expect(this.res.json).to.be.calledWith({ contact_ids: [] }) }) }) })