Merge pull request #18 from overleaf/spd-decaf

Decaffeinate contacts
This commit is contained in:
Simon Detheridge 2020-02-24 11:49:44 +00:00 committed by GitHub
commit bc80402eda
31 changed files with 4274 additions and 747 deletions

View file

@ -5,5 +5,3 @@ gitrev
.npm
.nvmrc
nodemon.json
app.js
**/js/*

View file

@ -0,0 +1,65 @@
// this file was auto-generated, do not edit it directly.
// instead run bin/update_build_scripts from
// https://github.com/sharelatex/sharelatex-dev-environment
// Version: 1.3.5
{
"extends": [
"standard",
"prettier",
"prettier/standard"
],
"parserOptions": {
"ecmaVersion": 2017
},
"plugins": [
"mocha",
"chai-expect",
"chai-friendly"
],
"env": {
"node": true,
"mocha": true
},
"rules": {
// Swap the no-unused-expressions rule with a more chai-friendly one
"no-unused-expressions": 0,
"chai-friendly/no-unused-expressions": "error"
},
"overrides": [
{
// Test specific rules
"files": ["test/**/*.js"],
"globals": {
"expect": true
},
"rules": {
// mocha-specific rules
"mocha/handle-done-callback": "error",
"mocha/no-exclusive-tests": "error",
"mocha/no-global-tests": "error",
"mocha/no-identical-title": "error",
"mocha/no-nested-tests": "error",
"mocha/no-pending-tests": "error",
"mocha/no-skipped-tests": "error",
"mocha/no-mocha-arrows": "error",
// chai-specific rules
"chai-expect/missing-assertion": "error",
"chai-expect/terminating-properties": "error",
// prefer-arrow-callback applies to all callbacks, not just ones in mocha tests.
// we don't enforce this at the top-level - just in tests to manage `this` scope
// based on mocha's context mechanism
"mocha/prefer-arrow-callback": "error"
}
},
{
// Backend specific rules
"files": ["app/**/*.js", "app.js", "index.js"],
"rules": {
// don't allow console.log in backend code
"no-console": "error"
}
}
]
}

View file

@ -1,7 +1,2 @@
node_modules
app/js/
test/unit/js
test/acceptance/js
app.js
**/*.map
forever

View file

@ -0,0 +1,8 @@
# This file was auto-generated, do not edit it directly.
# Instead run bin/update_build_scripts from
# https://github.com/sharelatex/sharelatex-dev-environment
# Version: 1.3.5
{
"semi": false,
"singleQuote": true
}

View file

@ -17,7 +17,6 @@ RUN npm install --quiet
COPY . /app
RUN npm run compile:all
FROM base

View file

@ -37,6 +37,13 @@ pipeline {
}
}
stage('Linting') {
steps {
sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make format'
sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make lint'
}
}
stage('Unit Tests') {
steps {
sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make test_unit'

View file

@ -16,12 +16,17 @@ DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \
clean:
docker rmi ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER)
docker rmi gcr.io/overleaf-ops/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER)
rm -f app.js
rm -rf app/js
rm -rf test/unit/js
rm -rf test/acceptance/js
test: test_unit test_acceptance
format:
$(DOCKER_COMPOSE) run --rm test_unit npm run format
format_fix:
$(DOCKER_COMPOSE) run --rm test_unit npm run format:fix
lint:
$(DOCKER_COMPOSE) run --rm test_unit npm run lint
test: format lint test_unit test_acceptance
test_unit:
@[ ! -d test/unit ] && echo "contacts has no unit tests" || $(DOCKER_COMPOSE) run --rm test_unit

View file

@ -1,45 +0,0 @@
Metrics = require "metrics-sharelatex"
Metrics.initialize("contacts")
Settings = require "settings-sharelatex"
logger = require "logger-sharelatex"
express = require "express"
bodyParser = require "body-parser"
Errors = require "./app/js/Errors"
HttpController = require "./app/js/HttpController"
Path = require "path"
logger.initialize("contacts")
Metrics.event_loop?.monitor(logger)
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 (error, req, res, next) ->
logger.error err: error, "request errored"
if error instanceof Errors.NotFoundError
res.send 404
else
res.send(500, "Oops, something went wrong")
port = Settings.internal.contacts.port
host = Settings.internal.contacts.host
if !module.parent # Called directly
app.listen port, host, (error) ->
throw error if error?
logger.info "contacts starting up, listening on #{host}:#{port}"
module.exports = app

59
services/contacts/app.js Normal file
View file

@ -0,0 +1,59 @@
/*
* 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('metrics-sharelatex')
Metrics.initialize('contacts')
const Settings = require('settings-sharelatex')
const logger = require('logger-sharelatex')
const express = require('express')
const bodyParser = require('body-parser')
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)
}
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.send(404)
} else {
return res.send(500, 'Oops, something went wrong')
}
})
const { port } = Settings.internal.contacts
const { host } = Settings.internal.contacts
if (!module.parent) {
// Called directly
app.listen(port, host, function(error) {
if (error != null) {
throw error
}
return logger.info(`contacts starting up, listening on ${host}:${port}`)
})
}
module.exports = app

View file

@ -1,38 +0,0 @@
{db, ObjectId} = require "./mongojs"
logger = require('logger-sharelatex')
metrics = require('metrics-sharelatex')
module.exports = ContactManager =
touchContact: (user_id, contact_id, callback = (error) ->) ->
try
user_id = ObjectId(user_id.toString())
catch error
return callback error
update = { $set: {}, $inc: {} }
update.$inc["contacts.#{contact_id}.n"] = 1
update.$set["contacts.#{contact_id}.ts"] = new Date()
db.contacts.update({
user_id: user_id
}, update, {
upsert: true
}, callback)
getContacts: (user_id, callback = (error) ->) ->
try
user_id = ObjectId(user_id.toString())
catch error
return callback error
db.contacts.findOne {
user_id: user_id
}, (error, user) ->
return callback(error) if error?
callback null, user?.contacts
[
'touchContact',
'getContacts',
].map (method) ->
metrics.timeAsyncMethod(ContactManager, method, 'mongo.ContactManager', logger)

View file

@ -1,10 +0,0 @@
NotFoundError = (message) ->
error = new Error(message)
error.name = "NotFoundError"
error.__proto__ = NotFoundError.prototype
return error
NotFoundError.prototype.__proto__ = Error.prototype
module.exports = Errors =
NotFoundError: NotFoundError

View file

@ -1,66 +0,0 @@
ContactManager = require "./ContactManager"
logger = require "logger-sharelatex"
module.exports = HttpController =
addContact: (req, res, next) ->
{user_id} = req.params
{contact_id} = req.body
if !contact_id? or contact_id == ""
res.status(400).send("contact_id should be a non-blank string")
return
logger.log {user_id, contact_id}, "adding contact"
ContactManager.touchContact user_id, contact_id, (error) ->
return next(error) if error?
ContactManager.touchContact contact_id, user_id, (error) ->
return next(error) if error?
res.status(204).end()
CONTACT_LIMIT: 50
getContacts: (req, res, next) ->
{user_id} = req.params
if req.query?.limit?
limit = parseInt(req.query.limit, 10)
else
limit = HttpController.CONTACT_LIMIT
limit = Math.min(limit, HttpController.CONTACT_LIMIT)
logger.log {user_id}, "getting contacts"
ContactManager.getContacts user_id, (error, contact_dict) ->
return next(error) if error?
contacts = []
for user_id, data of (contact_dict or {})
contacts.push {
user_id: user_id
n: data.n
ts: data.ts
}
HttpController._sortContacts contacts
contacts = contacts.slice(0, limit)
contact_ids = contacts.map (contact) -> contact.user_id
res.status(200).send({
contact_ids: contact_ids
})
_sortContacts: (contacts) ->
contacts.sort (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

@ -1,7 +0,0 @@
Settings = require "settings-sharelatex"
mongojs = require "mongojs"
db = mongojs(Settings.mongo.url, ["contacts"])
module.exports =
db: db
ObjectId: mongojs.ObjectId

View file

@ -0,0 +1,77 @@
/* eslint-disable
camelcase,
handle-callback-err,
*/
// 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('./mongojs')
const logger = require('logger-sharelatex')
const metrics = require('metrics-sharelatex')
module.exports = ContactManager = {
touchContact(user_id, contact_id, callback) {
if (callback == null) {
callback = function(error) {}
}
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()
return db.contacts.update(
{
user_id
},
update,
{
upsert: true
},
callback
)
},
getContacts(user_id, callback) {
if (callback == null) {
callback = function(error) {}
}
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
)
)

View file

@ -0,0 +1,16 @@
/* 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
var NotFoundError = function(message) {
const error = new Error(message)
error.name = 'NotFoundError'
error.__proto__ = NotFoundError.prototype
return error
}
NotFoundError.prototype.__proto__ = Error.prototype
module.exports = Errors = { NotFoundError }

View file

@ -0,0 +1,100 @@
/* 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('logger-sharelatex')
module.exports = HttpController = {
addContact(req, res, next) {
const { user_id } = req.params
const { contact_id } = req.body
if (contact_id == null || contact_id === '') {
res.status(400).send('contact_id should be a non-blank string')
return
}
logger.log({ user_id, contact_id }, 'adding contact')
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.status(204).end()
})
})
},
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.log({ 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
})
})
},
_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,9 @@
// TODO: This file was created by bulk-decaffeinate.
// Sanity-check the conversion and remove this comment.
const Settings = require('settings-sharelatex')
const mongojs = require('mongojs')
const db = mongojs(Settings.mongo.url, ['contacts'])
module.exports = {
db,
ObjectId: mongojs.ObjectId
}

View file

@ -1,10 +1,10 @@
contacts
--public-repo=False
--language=coffeescript
--env-add=
--node-version=10.19.0
--acceptance-creds=None
--dependencies=mongo
--docker-repos=gcr.io/overleaf-ops
--env-pass-through=
--script-version=1.3.5
--dependencies=mongo
--language=es
--docker-repos=gcr.io/overleaf-ops
--acceptance-creds=None
--env-pass-through=
--env-add=
--public-repo=False

View file

@ -1,11 +0,0 @@
http = require('http')
http.globalAgent.maxSockets = 300
module.exports =
internal:
contacts:
port: 3036
host: process.env["LISTEN_ADDRESS"] or "localhost"
mongo:
url: process.env['MONGO_CONNECTION_STRING'] or "mongodb://#{process.env["MONGO_HOST"] or "localhost"}/sharelatex"

View file

@ -0,0 +1,17 @@
const http = require('http')
http.globalAgent.maxSockets = 300
module.exports = {
internal: {
contacts: {
port: 3036,
host: process.env.LISTEN_ADDRESS || 'localhost'
}
},
mongo: {
url:
process.env.MONGO_CONNECTION_STRING ||
`mongodb://${process.env.MONGO_HOST || 'localhost'}/sharelatex`
}
}

View file

@ -10,10 +10,9 @@
},
"watch": [
"app/coffee/",
"app.coffee",
"app/js/",
"app.js",
"config/"
],
"ext": "coffee"
"ext": "js"
}

File diff suppressed because it is too large Load diff

View file

@ -8,34 +8,49 @@
"url": "https://github.com/sharelatex/contacts-sharelatex.git"
},
"scripts": {
"compile:app": "([ -e app/coffee ] && coffee -m $COFFEE_OPTIONS -o app/js -c app/coffee || echo 'No CoffeeScript folder to compile') && ( [ -e app.coffee ] && coffee -m $COFFEE_OPTIONS -c app.coffee || echo 'No CoffeeScript app to compile')",
"start": "npm run compile:app && node $NODE_APP_OPTIONS app.js",
"test:acceptance:_run": "mocha --recursive --reporter spec --timeout 30000 --exit $@ test/acceptance/js",
"test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
"test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js",
"test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- --grep=$MOCHA_GREP",
"compile:unit_tests": "[ ! -e test/unit/coffee ] && echo 'No unit tests to compile' || coffee -o test/unit/js -c test/unit/coffee",
"compile:acceptance_tests": "[ ! -e test/acceptance/coffee ] && echo 'No acceptance tests to compile' || coffee -o test/acceptance/js -c test/acceptance/coffee",
"compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests && npm run compile:smoke_tests",
"start": "node $NODE_APP_OPTIONS app.js",
"test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
"test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
"test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js",
"test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
"nodemon": "nodemon --config nodemon.json",
"compile:smoke_tests": "[ ! -e test/smoke/coffee ] && echo 'No smoke tests to compile' || coffee -o test/smoke/js -c test/smoke/coffee"
"lint": "node_modules/.bin/eslint .",
"format": "node_modules/.bin/prettier-eslint '**/*.js' --list-different",
"format:fix": "node_modules/.bin/prettier-eslint '**/*.js' --write"
},
"dependencies": {
"async": "~0.8.0",
"body-parser": "~1.0.2",
"coffee-script": "^1.7.1",
"express": "~4.1.1",
"express": "~4.5.0",
"logger-sharelatex": "^1.7.0",
"metrics-sharelatex": "^2.2.0",
"mongojs": "2.4.0",
"request": "~2.34.0",
"request": "~2.47.0",
"settings-sharelatex": "^1.1.0",
"underscore": "~1.6.0"
},
"devDependencies": {
"babel-eslint": "^10.0.3",
"bunyan": "~0.22.3",
"chai": "~1.9.1",
"eslint": "^6.6.0",
"eslint-config-prettier": "^6.10.0",
"eslint-config-standard": "^14.1.0",
"eslint-config-standard-jsx": "^8.1.0",
"eslint-config-standard-react": "^9.2.0",
"eslint-plugin-chai-expect": "^2.1.0",
"eslint-plugin-chai-friendly": "^0.5.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-mocha": "^6.2.2",
"eslint-plugin-node": "^11.0.0",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-react": "^7.18.3",
"eslint-plugin-standard": "^4.0.1",
"mocha": "^4.0.1",
"prettier": "^1.19.1",
"prettier-eslint-cli": "^5.0.0",
"sandboxed-module": "~0.3.0",
"sinon": "~1.5.2",
"timekeeper": "0.0.5"

View file

@ -1,20 +0,0 @@
app = require('../../../app')
require("logger-sharelatex").logger.level("error")
module.exports =
running: false
initing: false
callbacks: []
ensureRunning: (callback = (error) ->) ->
if @running
return callback()
else if @initing
@callbacks.push callback
else
@initing = true
@callbacks.push callback
app.listen 3036, "localhost", (error) =>
throw error if error?
@running = true
for callback in @callbacks
callback()

View file

@ -1,71 +0,0 @@
sinon = require "sinon"
chai = require("chai")
chai.should()
expect = chai.expect
ObjectId = require("mongojs").ObjectId
request = require "request"
async = require "async"
ContactsApp = require "./ContactsApp"
HOST = "http://localhost:3036"
describe "Getting Contacts", ->
describe "with no contacts", ->
beforeEach (done)->
@user_id = ObjectId().toString()
ContactsApp.ensureRunning done
it "should return an empty array", (done) ->
request {
method: "GET"
url: "#{HOST}/user/#{@user_id}/contacts"
json: true
}, (error, response, body) ->
response.statusCode.should.equal 200
body.contact_ids.should.deep.equal []
done()
describe "with contacts", ->
beforeEach (done) ->
@user_id = ObjectId().toString()
@contact_id_1 = ObjectId().toString()
@contact_id_2 = ObjectId().toString()
@contact_id_3 = ObjectId().toString()
touchContact = (user_id, contact_id, cb) ->
request({
method: "POST"
url: "#{HOST}/user/#{user_id}/contacts"
json: {
contact_id: contact_id
}
}, cb)
async.series [
# 2 is preferred since touched twice, then 3 since most recent, then 1
(cb) => ContactsApp.ensureRunning cb
(cb) => touchContact @user_id, @contact_id_1, cb
(cb) => touchContact @user_id, @contact_id_2, cb
(cb) => touchContact @user_id, @contact_id_2, cb
(cb) => touchContact @user_id, @contact_id_3, cb
], done
it "should return a sorted list of contacts", (done) ->
request {
method: "GET"
url: "#{HOST}/user/#{@user_id}/contacts"
json: true
}, (error, response, body) =>
response.statusCode.should.equal 200
body.contact_ids.should.deep.equal [@contact_id_2, @contact_id_3, @contact_id_1]
done()
it "should respect a limit and only return top X contacts", ->
request {
method: "GET"
url: "#{HOST}/user/#{@user_id}/contacts?limit=2"
json: true
}, (error, response, body) =>
response.statusCode.should.equal 200
body.contact_ids.should.deep.equal [@contact_id_2, @contact_id_3]
done()

View file

@ -0,0 +1,47 @@
/* eslint-disable
handle-callback-err,
*/
// 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')
require('logger-sharelatex').logger.level('error')
module.exports = {
running: false,
initing: false,
callbacks: [],
ensureRunning(callback) {
if (callback == null) {
callback = function(error) {}
}
if (this.running) {
return callback()
} else if (this.initing) {
return this.callbacks.push(callback)
} else {
this.initing = true
this.callbacks.push(callback)
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

@ -0,0 +1,116 @@
/* eslint-disable
camelcase,
handle-callback-err,
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('mongojs')
const request = require('request')
const async = require('async')
const ContactsApp = require('./ContactsApp')
const HOST = 'http://localhost:3036'
describe('Getting Contacts', function() {
describe('with no contacts', function() {
beforeEach(function(done) {
this.user_id = ObjectId().toString()
return ContactsApp.ensureRunning(done)
})
return it('should return an empty array', function(done) {
return request(
{
method: 'GET',
url: `${HOST}/user/${this.user_id}/contacts`,
json: true
},
(error, response, body) => {
response.statusCode.should.equal(200)
body.contact_ids.should.deep.equal([])
return done()
}
)
})
})
return 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) =>
request(
{
method: 'POST',
url: `${HOST}/user/${user_id}/contacts`,
json: {
contact_id
}
},
cb
)
return 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),
cb => touchContact(this.user_id, this.contact_id_3, cb)
],
done
)
})
it('should return a sorted list of contacts', function(done) {
return request(
{
method: 'GET',
url: `${HOST}/user/${this.user_id}/contacts`,
json: true
},
(error, response, body) => {
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()
}
)
})
return it('should respect a limit and only return top X contacts', function() {
return request(
{
method: 'GET',
url: `${HOST}/user/${this.user_id}/contacts?limit=2`,
json: true
},
(error, response, body) => {
response.statusCode.should.equal(200)
body.contact_ids.should.deep.equal([
this.contact_id_2,
this.contact_id_3
])
return done()
}
)
})
})
})

View file

@ -1,86 +0,0 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
expect = chai.expect
modulePath = "../../../app/js/ContactManager.js"
SandboxedModule = require('sandboxed-module')
ObjectId = require("mongojs").ObjectId
tk = require("timekeeper")
describe "ContactManager", ->
beforeEach ->
tk.freeze(Date.now())
@ContactManager = SandboxedModule.require modulePath, requires:
"./mongojs": {
db: @db = contacts: {}
ObjectId: ObjectId
},
'logger-sharelatex': {log: sinon.stub()},
'metrics-sharelatex': {timeAsyncMethod: sinon.stub()}
@user_id = ObjectId().toString()
@contact_id = ObjectId().toString()
@callback = sinon.stub()
afterEach ->
tk.reset()
describe "touchContact", ->
beforeEach ->
@db.contacts.update = sinon.stub().callsArg(3)
describe "with a valid user_id", ->
beforeEach ->
@ContactManager.touchContact @user_id, @contact_id = "mock_contact", @callback
it "should increment the contact count and timestamp", ->
@db.contacts.update
.calledWith({
user_id: sinon.match((o) => o.toString() == @user_id.toString())
}, {
$inc:
"contacts.mock_contact.n": 1
$set:
"contacts.mock_contact.ts": new Date()
}, {
upsert: true
})
.should.equal true
it "should call the callback", ->
@callback.called.should.equal true
describe "with an invalid user id", ->
beforeEach ->
@ContactManager.touchContact "not-valid-object-id", @contact_id, @callback
it "should call the callback with an error", ->
@callback.calledWith(new Error()).should.equal true
describe "getContacts", ->
beforeEach ->
@user = {
contacts: ["mock", "contacts"]
}
@db.contacts.findOne = sinon.stub().callsArgWith(1, null, @user)
describe "with a valid user_id", ->
beforeEach ->
@ContactManager.getContacts @user_id, @callback
it "should find the user's contacts", ->
@db.contacts.findOne
.calledWith({
user_id: sinon.match((o) => o.toString() == @user_id.toString())
})
.should.equal true
it "should call the callback with the contacts", ->
@callback.calledWith(null, @user.contacts).should.equal true
describe "with an invalid user id", ->
beforeEach ->
@ContactManager.getContacts "not-valid-object-id", @callback
it "should call the callback with an error", ->
@callback.calledWith(new Error()).should.equal true

View file

@ -1,120 +0,0 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
expect = chai.expect
modulePath = "../../../app/js/HttpController.js"
SandboxedModule = require('sandboxed-module')
describe "HttpController", ->
beforeEach ->
@HttpController = SandboxedModule.require modulePath, requires:
"./ContactManager": @ContactManager = {}
"logger-sharelatex": @logger = { log: sinon.stub() }
@user_id = "mock-user-id"
@contact_id = "mock-contact-id"
@req = {}
@res = {}
@res.status = sinon.stub().returns @res
@res.end = sinon.stub()
@res.send = sinon.stub()
@next = sinon.stub()
describe "addContact", ->
beforeEach ->
@req.params =
user_id: @user_id
@ContactManager.touchContact = sinon.stub().callsArg(2)
describe "with a valid user_id and contact_id", ->
beforeEach ->
@req.body =
contact_id: @contact_id
@HttpController.addContact @req, @res, @next
it "should update the contact in the user's contact list", ->
@ContactManager.touchContact
.calledWith(@user_id, @contact_id)
.should.equal true
it "should update the user in the contact's contact list", ->
@ContactManager.touchContact
.calledWith(@contact_id, @user_id)
.should.equal true
it "should send back a 204 status", ->
@res.status.calledWith(204).should.equal true
@res.end.called.should.equal true
describe "with an invalid contact id", ->
beforeEach ->
@req.body =
contact_id: ""
@HttpController.addContact @req, @res, @next
it "should return 400, Bad Request", ->
@res.status.calledWith(400).should.equal true
@res.send.calledWith("contact_id should be a non-blank string").should.equal true
describe "getContacts", ->
beforeEach ->
@req.params =
user_id: @user_id
now = Date.now()
@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) }
}
@ContactManager.getContacts = sinon.stub().callsArgWith(1, null, @contacts)
describe "normally", ->
beforeEach ->
@HttpController.getContacts @req, @res, @next
it "should look up the contacts in mongo", ->
@ContactManager.getContacts
.calledWith(@user_id)
.should.equal true
it "should return a sorted list of contacts by count and timestamp", ->
@res.send
.calledWith({
contact_ids: [
"user-id-2"
"user-id-1"
"user-id-3"
]
})
.should.equal true
describe "with more contacts than the limit", ->
beforeEach ->
@req.query =
limit: 2
@HttpController.getContacts @req, @res, @next
it "should return the most commonly used contacts up to the limit", ->
@res.send
.calledWith({
contact_ids: [
"user-id-2"
"user-id-1"
]
})
.should.equal true
describe "without a contact list", ->
beforeEach ->
@ContactManager.getContacts = sinon.stub().callsArgWith(1, null, null)
@HttpController.getContacts @req, @res, @next
it "should return an empty list", ->
@res.send
.calledWith({
contact_ids: []
})
.should.equal true
describe "with a holding account", ->
it "should not return holding accounts"

View file

@ -0,0 +1,143 @@
/* 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('mongojs')
const tk = require('timekeeper')
describe('ContactManager', function() {
beforeEach(function() {
tk.freeze(Date.now())
this.ContactManager = SandboxedModule.require(modulePath, {
requires: {
'./mongojs': {
db: (this.db = { contacts: {} }),
ObjectId
},
'logger-sharelatex': { log: sinon.stub() },
'metrics-sharelatex': { timeAsyncMethod: sinon.stub() }
}
})
this.user_id = ObjectId().toString()
this.contact_id = ObjectId().toString()
return (this.callback = sinon.stub())
})
afterEach(function() {
return tk.reset()
})
describe('touchContact', function() {
beforeEach(function() {
return (this.db.contacts.update = sinon.stub().callsArg(3))
})
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', function() {
return this.db.contacts.update
.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
)
})
return it('should call the callback with an error', function() {
return this.callback.calledWith(new Error()).should.equal(true)
})
})
})
return describe('getContacts', function() {
beforeEach(function() {
this.user = {
contacts: ['mock', 'contacts']
}
return (this.db.contacts.findOne = sinon
.stub()
.callsArgWith(1, null, 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", 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)
})
})
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(new Error()).should.equal(true)
})
})
})
})

View file

@ -0,0 +1,150 @@
/* 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')
describe('HttpController', function() {
beforeEach(function() {
this.HttpController = SandboxedModule.require(modulePath, {
requires: {
'./ContactManager': (this.ContactManager = {}),
'logger-sharelatex': (this.logger = { log: sinon.stub() })
}
})
this.user_id = 'mock-user-id'
this.contact_id = 'mock-contact-id'
this.req = {}
this.res = {}
this.res.status = sinon.stub().returns(this.res)
this.res.end = sinon.stub()
this.res.send = sinon.stub()
return (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() {
this.req.body = { contact_id: this.contact_id }
return 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)
})
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)
})
return it('should send back a 204 status', function() {
this.res.status.calledWith(204).should.equal(true)
return this.res.end.called.should.equal(true)
})
})
return describe('with an invalid contact id', function() {
beforeEach(function() {
this.req.body = { contact_id: '' }
return 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)
})
})
})
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('normally', function() {
beforeEach(function() {
return 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)
})
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)
})
})
describe('with more contacts than the limit', function() {
beforeEach(function() {
this.req.query = { limit: 2 }
return 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)
})
})
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)
})
return it('should return an empty list', function() {
return this.res.send
.calledWith({
contact_ids: []
})
.should.equal(true)
})
})
})
})