Migrate contacts service to ES modules (#10904)

GitOrigin-RevId: c5abb64729530baecbee0eb589eaed39faa2ac56
This commit is contained in:
Alf Eaton 2022-12-16 10:42:39 +00:00 committed by Copybot
parent bcf20abbc2
commit c14467b87a
16 changed files with 423 additions and 576 deletions

107
package-lock.json generated
View file

@ -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",

View file

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

View file

@ -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

View file

@ -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
}

View file

@ -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 }

View file

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

View file

@ -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
}

View file

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

View file

@ -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')
}
})

View file

@ -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`,

View file

@ -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"
}
}

View file

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

View file

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

View file

@ -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)

View file

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

View file

@ -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: [] })
})
})
})