diff --git a/services/contacts/app/coffee/ContactManager.coffee b/services/contacts/app/coffee/ContactManager.coffee index df5bc1ff77..918518fcf4 100644 --- a/services/contacts/app/coffee/ContactManager.coffee +++ b/services/contacts/app/coffee/ContactManager.coffee @@ -15,4 +15,16 @@ module.exports = ContactManager = user_id: user_id }, update, { upsert: true - }, callback) \ No newline at end of file + }, 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 \ No newline at end of file diff --git a/services/contacts/app/coffee/HttpController.coffee b/services/contacts/app/coffee/HttpController.coffee index d160f76bec..dd3f21791e 100644 --- a/services/contacts/app/coffee/HttpController.coffee +++ b/services/contacts/app/coffee/HttpController.coffee @@ -1,5 +1,7 @@ ContactManager = require "./ContactManager" +WebApiManager = require "./WebApiManager" logger = require "logger-sharelatex" +async = require "async" module.exports = HttpController = addContact: (req, res, next) -> @@ -18,5 +20,63 @@ module.exports = HttpController = return next(error) if error? res.status(204).end() - getUserContacts: (req, res, next) -> - \ No newline at end of file + 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) + + async.mapLimit contacts, 5, + (contact, cb) -> + WebApiManager.getUserDetails contact.user_id, (error, user) -> + return cb(error) if error? + cb null, HttpController._formatUser user + (error, users) -> + return next(error) if error? + res.status(200).send({ + contacts: users + }) + + _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 + + _formatUser: (user) -> + return { + id: user._id + email: user.email + first_name: user.first_name + last_name: user.last_name + } \ No newline at end of file diff --git a/services/contacts/test/unit/coffee/ContactsManagerTests.coffee b/services/contacts/test/unit/coffee/ContactsManagerTests.coffee index 4d9f89e860..a6a20f6501 100644 --- a/services/contacts/test/unit/coffee/ContactsManagerTests.coffee +++ b/services/contacts/test/unit/coffee/ContactsManagerTests.coffee @@ -35,13 +35,14 @@ describe "ContactManager", -> .calledWith({ user_id: ObjectId(@user_id) }, { - $set: + $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 @@ -52,3 +53,32 @@ describe "ContactManager", -> 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: ObjectId(@user_id) + }) + .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 + diff --git a/services/contacts/test/unit/coffee/HttpControllerTests.coffee b/services/contacts/test/unit/coffee/HttpControllerTests.coffee index 257de0eb05..17fea0e71e 100644 --- a/services/contacts/test/unit/coffee/HttpControllerTests.coffee +++ b/services/contacts/test/unit/coffee/HttpControllerTests.coffee @@ -9,6 +9,7 @@ describe "HttpController", -> beforeEach -> @HttpController = SandboxedModule.require modulePath, requires: "./ContactManager": @ContactManager = {} + "./WebApiManager": @WebApiManager = {} "logger-sharelatex": @logger = { log: sinon.stub() } @user_id = "mock-user-id" @contact_id = "mock-contact-id" @@ -55,3 +56,80 @@ describe "HttpController", -> 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) } + } + @user_details = { + "user-id-1": { _id: "user-id-1", email: "joe@example.com", first_name: "Joe", last_name: "Example", extra: "foo" } + "user-id-2": { _id: "user-id-2", email: "jane@example.com", first_name: "Sarah", last_name: "Example", extra: "foo" } + "user-id-3": { _id: "user-id-3", email: "sam@example.com", first_name: "Sam", last_name: "Example", extra: "foo" } + } + @ContactManager.getContacts = sinon.stub().callsArgWith(1, null, @contacts) + @WebApiManager.getUserDetails = (user_id, callback = (error, user) ->) => + callback null, @user_details[user_id] + sinon.spy @WebApiManager, "getUserDetails" + + 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 look up each contact in web for their details", -> + for user_id, data of @contacts + @WebApiManager.getUserDetails + .calledWith(user_id) + .should.equal true + + it "should return a sorted list of contacts by count and timestamp", -> + @res.send + .calledWith({ + contacts: [ + { id: "user-id-2", email: "jane@example.com", first_name: "Sarah", last_name: "Example" } + { id: "user-id-1", email: "joe@example.com", first_name: "Joe", last_name: "Example" } + { id: "user-id-3", email: "sam@example.com", first_name: "Sam", last_name: "Example" } + ] + }) + .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({ + contacts: [ + { id: "user-id-2", email: "jane@example.com", first_name: "Sarah", last_name: "Example" } + { id: "user-id-1", email: "joe@example.com", first_name: "Joe", last_name: "Example" } + ] + }) + .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({ + contacts: [] + }) + .should.equal true + + describe "with a holding account", -> + it "should not return holding accounts" \ No newline at end of file