Przeglądaj źródła

Migrate contacts service to ES modules (#10904)

GitOrigin-RevId: c5abb64729530baecbee0eb589eaed39faa2ac56
Alf Eaton 3 lat temu
rodzic
commit
c14467b87a

+ 101 - 6
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",

+ 6 - 0
services/contacts/.eslintrc

@@ -0,0 +1,6 @@
+{
+  "parserOptions": {
+    "ecmaVersion": 2022,
+    "sourceType": "module"
+  }
+}

+ 17 - 67
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')
-
-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')
-
-logger.initialize('contacts')
-if (Metrics.event_loop != null) {
-  Metrics.event_loop.monitor(logger)
+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 { host, port } = Settings.internal.contacts
+
+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

+ 18 - 70
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
-    )
-  },
+    },
+    { upsert: true }
+  )
+}
 
-  getContacts(user_id, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    try {
-      user_id = ObjectId(user_id.toString())
-    } catch (error1) {
-      const error = error1
-      return callback(error)
-    }
+export async function getContacts(userId) {
+  const user = await db.contacts.findOne({
+    user_id: ObjectId(userId.toString()),
+  })
 
-    return db.contacts.findOne(
-      {
-        user_id,
-      },
-      function (error, user) {
-        if (error != null) {
-          return callback(error)
-        }
-        return callback(null, user != null ? user.contacts : undefined)
-      }
-    )
-  },
+  return user?.contacts
 }
-;['touchContact', 'getContacts'].map(method =>
-  metrics.timeAsyncMethod(
-    ContactManager,
-    method,
-    'mongo.ContactManager',
-    logger
-  )
-)

+ 5 - 15
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 }

+ 35 - 87
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)
-      })
-    })
-  },
-
-  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: userId, contact_id: contactId }, 'adding contact')
 
-    logger.debug({ user_id }, 'getting contacts')
+  Promise.all([
+    ContactManager.touchContact(userId, contactId),
+    ContactManager.touchContact(contactId, userId),
+  ])
+    .then(() => {
+      res.sendStatus(204)
+    })
+    .catch(error => {
+      next(error)
+    })
+}
 
-    return ContactManager.getContacts(user_id, function (error, contact_dict) {
-      if (error != null) {
-        return next(error)
-      }
+export function getContacts(req, res, next) {
+  const { user_id: userId } = req.params
+  const { limit } = req.query
 
-      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,
-        })
-      }
+  const contactLimit =
+    limit == null ? CONTACT_LIMIT : Math.min(parseInt(limit, 10), CONTACT_LIMIT)
 
-      HttpController._sortContacts(contacts)
-      contacts = contacts.slice(0, limit)
-      const contact_ids = contacts.map(contact => contact.user_id)
+  logger.debug({ user_id: userId }, 'getting contacts')
 
-      return res.status(200).send({
-        contact_ids,
+  ContactManager.getContacts(userId)
+    .then(contacts => {
+      res.json({
+        contact_ids: buildContactIds(contacts, contactLimit),
       })
     })
-  },
-
-  _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
-        }
-      }
+    .catch(error => {
+      next(error)
     })
-  },
 }

+ 13 - 0
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
+}

+ 7 - 23
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()
+export const mongoClient = new MongoClient(Settings.mongo.url)
 
-  db.contacts = internalDb.collection('contacts')
-}
+const mongoDb = mongoClient.db()
 
-module.exports = {
-  db,
-  ObjectId,
-  waitForDb,
+export const db = {
+  contacts: mongoDb.collection('contacts'),
 }

+ 32 - 0
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')
+  }
+})

+ 0 - 4
services/contacts/config/settings.defaults.js → 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`,

+ 6 - 5
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"
   }
 }

+ 0 - 46
services/contacts/test/acceptance/js/ContactsApp.js

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

+ 42 - 39
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()
         }
       )
     })

+ 6 - 14
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)

+ 56 - 99
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
 
-      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()
-              ),
+        expect(this.db.contacts.updateOne).to.be.calledWith(
+          {
+            user_id: sinon.match(o => o.toString() === this.user_id),
+          },
+          {
+            $inc: {
+              'contacts.mock_contact.n': 1,
             },
-            {
-              $inc: {
-                'contacts.mock_contact.n': 1,
-              },
-              $set: {
-                'contacts.mock_contact.ts': new Date(),
-              },
+            $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)
+          },
+          {
+            upsert: 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
       })
     })
   })

+ 69 - 91
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: [] })
       })
     })
   })