mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Migrate contacts service to ES modules (#10904)
GitOrigin-RevId: c5abb64729530baecbee0eb589eaed39faa2ac56
This commit is contained in:
parent
bcf20abbc2
commit
c14467b87a
16 changed files with 423 additions and 576 deletions
107
package-lock.json
generated
107
package-lock.json
generated
|
@ -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
services/contacts/.eslintrc
Normal file
6
services/contacts/.eslintrc
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2022,
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
|
13
services/contacts/app/js/contacts.js
Normal file
13
services/contacts/app/js/contacts.js
Normal 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
|
||||
}
|
|
@ -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'),
|
||||
}
|
||||
|
|
32
services/contacts/app/js/server.js
Normal file
32
services/contacts/app/js/server.js
Normal 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')
|
||||
}
|
||||
})
|
|
@ -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`,
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})()
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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: [] })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue