Merge pull request #2614 from overleaf/sk-monolithify-tags

Move 'tags' into web

GitOrigin-RevId: a248d1b2471f0bfa05589df9b7357b4d85793a79
This commit is contained in:
Shane Kilkelly
2020-03-24 10:20:06 +00:00
committed by Copybot
parent bffb9f3bce
commit b51e3c01e4
11 changed files with 755 additions and 592 deletions
@@ -1,93 +1,88 @@
/* eslint-disable
camelcase,
max-len,
*/
// 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
*/
const TagsHandler = require('./TagsHandler')
const AuthenticationController = require('../Authentication/AuthenticationController')
const Errors = require('../Errors/Errors')
module.exports = {
getAllTags(req, res, next) {
const user_id = AuthenticationController.getLoggedInUserId(req)
return TagsHandler.getAllTags(user_id, function(error, allTags) {
const TagsController = {
_getTags(userId, _req, res, next) {
if (!userId) {
return next(new Errors.NotFoundError())
}
TagsHandler.getAllTags(userId, function(error, allTags) {
if (error != null) {
return next(error)
}
return res.json(allTags)
res.json(allTags)
})
},
apiGetAllTags(req, res, next) {
const { userId } = req.params
TagsController._getTags(userId, req, res, next)
},
getAllTags(req, res, next) {
const userId = AuthenticationController.getLoggedInUserId(req)
TagsController._getTags(userId, req, res, next)
},
createTag(req, res, next) {
const user_id = AuthenticationController.getLoggedInUserId(req)
const userId = AuthenticationController.getLoggedInUserId(req)
const { name } = req.body
return TagsHandler.createTag(user_id, name, function(error, tag) {
TagsHandler.createTag(userId, name, function(error, tag) {
if (error != null) {
return next(error)
}
return res.json(tag)
res.json(tag)
})
},
addProjectToTag(req, res, next) {
const user_id = AuthenticationController.getLoggedInUserId(req)
const { tag_id, project_id } = req.params
return TagsHandler.addProjectToTag(user_id, tag_id, project_id, function(
error
) {
if (error != null) {
const userId = AuthenticationController.getLoggedInUserId(req)
const { tagId, projectId } = req.params
TagsHandler.addProjectToTag(userId, tagId, projectId, function(error) {
if (error) {
return next(error)
}
return res.status(204).end()
res.status(204).end()
})
},
removeProjectFromTag(req, res, next) {
const user_id = AuthenticationController.getLoggedInUserId(req)
const { tag_id, project_id } = req.params
return TagsHandler.removeProjectFromTag(
user_id,
tag_id,
project_id,
function(error) {
if (error != null) {
return next(error)
}
return res.status(204).end()
const userId = AuthenticationController.getLoggedInUserId(req)
const { tagId, projectId } = req.params
TagsHandler.removeProjectFromTag(userId, tagId, projectId, function(error) {
if (error) {
return next(error)
}
)
res.status(204).end()
})
},
deleteTag(req, res, next) {
const user_id = AuthenticationController.getLoggedInUserId(req)
const { tag_id } = req.params
return TagsHandler.deleteTag(user_id, tag_id, function(error) {
if (error != null) {
const userId = AuthenticationController.getLoggedInUserId(req)
const { tagId } = req.params
TagsHandler.deleteTag(userId, tagId, function(error) {
if (error) {
return next(error)
}
return res.status(204).end()
res.status(204).end()
})
},
renameTag(req, res, next) {
const user_id = AuthenticationController.getLoggedInUserId(req)
const { tag_id } = req.params
const userId = AuthenticationController.getLoggedInUserId(req)
const { tagId } = req.params
const name = req.body != null ? req.body.name : undefined
if (name == null) {
if (!name) {
return res.status(400).end()
} else {
return TagsHandler.renameTag(user_id, tag_id, name, function(error) {
if (error != null) {
return next(error)
}
return res.status(204).end()
})
}
TagsHandler.renameTag(userId, tagId, name, function(error) {
if (error) {
return next(error)
}
res.status(204).end()
})
}
}
module.exports = TagsController
+67 -107
View File
@@ -1,144 +1,104 @@
const settings = require('settings-sharelatex')
const request = require('request')
const logger = require('logger-sharelatex')
const { Tag } = require('../../models/Tag')
const { promisifyAll } = require('../../util/promises')
const TIMEOUT = 10000
function getAllTags(userId, callback) {
const opts = {
url: `${settings.apis.tags.url}/user/${userId}/tag`,
json: true,
timeout: TIMEOUT
}
request.get(opts, (err, res, body) =>
_handleResponse(err, res, { userId }, function(error) {
if (error != null) {
return callback(error, [])
}
callback(null, body || [])
})
)
Tag.find({ user_id: userId }, callback)
}
function createTag(userId, name, callback) {
const opts = {
url: `${settings.apis.tags.url}/user/${userId}/tag`,
json: {
name
},
timeout: TIMEOUT
if (!callback) {
callback = function() {}
}
request.post(opts, (err, res, body) =>
_handleResponse(err, res, { userId }, function(error) {
if (error != null) {
return callback(error)
}
callback(null, body || {})
})
)
Tag.create({ user_id: userId, name }, function(err, tag) {
// on duplicate key error return existing tag
if (err && err.code === 11000) {
return Tag.findOne({ user_id: userId, name }, callback)
}
callback(err, tag)
})
}
function renameTag(userId, tagId, name, callback) {
const url = `${settings.apis.tags.url}/user/${userId}/tag/${tagId}/rename`
request.post(
if (!callback) {
callback = function() {}
}
Tag.update(
{
url,
json: {
name
},
timeout: TIMEOUT
_id: tagId,
user_id: userId
},
(err, res, body) =>
_handleResponse(err, res, { url, userId, tagId, name }, callback)
{
$set: {
name
}
},
callback
)
}
function deleteTag(userId, tagId, callback) {
const url = `${settings.apis.tags.url}/user/${userId}/tag/${tagId}`
request.del({ url, timeout: TIMEOUT }, (err, res, body) =>
_handleResponse(err, res, { url, userId, tagId }, callback)
if (!callback) {
callback = function() {}
}
Tag.remove(
{
_id: tagId,
user_id: userId
},
callback
)
}
// TODO: unused?
function updateTagUserIds(oldUserId, newUserId, callback) {
const opts = {
url: `${settings.apis.tags.url}/user/${oldUserId}/tag`,
json: {
user_id: newUserId
},
timeout: TIMEOUT
if (!callback) {
callback = function() {}
}
request.put(opts, (err, res, body) =>
_handleResponse(err, res, { oldUserId, newUserId }, callback)
)
const searchOps = { user_id: oldUserId }
const updateOperation = { $set: { user_id: newUserId } }
Tag.update(searchOps, updateOperation, { multi: true }, callback)
}
function removeProjectFromTag(userId, tagId, projectId, callback) {
const url = `${
settings.apis.tags.url
}/user/${userId}/tag/${tagId}/project/${projectId}`
request.del({ url, timeout: TIMEOUT }, (err, res, body) =>
_handleResponse(err, res, { url, userId, tagId, projectId }, callback)
)
if (!callback) {
callback = function() {}
}
const searchOps = {
_id: tagId,
user_id: userId
}
const deleteOperation = { $pull: { project_ids: projectId } }
Tag.update(searchOps, deleteOperation, callback)
}
function addProjectToTag(userId, tagId, projectId, callback) {
const url = `${
settings.apis.tags.url
}/user/${userId}/tag/${tagId}/project/${projectId}`
request.post({ url, timeout: TIMEOUT }, (err, res, body) =>
_handleResponse(err, res, { url, userId, tagId, projectId }, callback)
)
if (!callback) {
callback = function() {}
}
const searchOps = {
_id: tagId,
user_id: userId
}
const insertOperation = { $addToSet: { project_ids: projectId } }
Tag.findOneAndUpdate(searchOps, insertOperation, callback)
}
function addProjectToTagName(userId, name, projectId, callback) {
const url = `${
settings.apis.tags.url
}/user/${userId}/tag/project/${projectId}`
const opts = {
json: { name },
timeout: TIMEOUT,
url
if (!callback) {
callback = function() {}
}
request.post(opts, (err, res, body) =>
_handleResponse(err, res, { url, userId, name, projectId }, callback)
)
const searchOps = {
name,
user_id: userId
}
const insertOperation = { $addToSet: { project_ids: projectId } }
Tag.update(searchOps, insertOperation, { upsert: true }, callback)
}
function removeProjectFromAllTags(userId, projectId, callback) {
const url = `${settings.apis.tags.url}/user/${userId}/project/${projectId}`
const opts = {
url,
timeout: TIMEOUT
}
request.del(opts, (err, res, body) =>
_handleResponse(err, res, { url, userId, projectId }, callback)
)
}
function _handleResponse(err, res, params, callback) {
if (err != null) {
params.err = err
logger.warn(params, 'error in tag api')
return callback(err)
} else if (res != null && res.statusCode >= 200 && res.statusCode < 300) {
return callback(null)
} else {
err = new Error(
`tags api returned a failure status code: ${
res != null ? res.statusCode : undefined
}`
)
params.err = err
logger.warn(
params,
`tags api returned failure status code: ${
res != null ? res.statusCode : undefined
}`
)
callback(err)
}
const searchOps = { user_id: userId }
const deleteOperation = { $pull: { project_ids: projectId } }
Tag.update(searchOps, deleteOperation, { multi: true }, callback)
}
const TagsHandler = {
+14
View File
@@ -0,0 +1,14 @@
const mongoose = require('../infrastructure/Mongoose')
const { Schema } = mongoose
// Note that for legacy reasons, user_id and project_ids are plain strings,
// not ObjectIds.
const TagSchema = new Schema({
user_id: { type: String, required: true },
name: { type: String, required: true },
project_ids: [String]
})
exports.Tag = mongoose.model('Tag', TagSchema)
exports.TagSchema = TagSchema
+9 -4
View File
@@ -628,6 +628,11 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
UserController.expireDeletedUser
)
privateApiRouter.get(
'/user/:userId/tag',
AuthenticationController.httpAuth,
TagsController.apiGetAllTags
)
webRouter.get(
'/tag',
AuthenticationController.requireLogin(),
@@ -644,7 +649,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
TagsController.createTag
)
webRouter.post(
'/tag/:tag_id/rename',
'/tag/:tagId/rename',
AuthenticationController.requireLogin(),
RateLimiterMiddleware.rateLimit({
endpointName: 'rename-tag',
@@ -654,7 +659,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
TagsController.renameTag
)
webRouter.delete(
'/tag/:tag_id',
'/tag/:tagId',
AuthenticationController.requireLogin(),
RateLimiterMiddleware.rateLimit({
endpointName: 'delete-tag',
@@ -664,7 +669,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
TagsController.deleteTag
)
webRouter.post(
'/tag/:tag_id/project/:project_id',
'/tag/:tagId/project/:projectId',
AuthenticationController.requireLogin(),
RateLimiterMiddleware.rateLimit({
endpointName: 'add-project-to-tag',
@@ -674,7 +679,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
TagsController.addProjectToTag
)
webRouter.delete(
'/tag/:tag_id/project/:project_id',
'/tag/:tagId/project/:projectId',
AuthenticationController.requireLogin(),
RateLimiterMiddleware.rateLimit({
endpointName: 'remove-project-from-tag',
@@ -7,7 +7,6 @@ const { db, ObjectId } = require('../../../app/src/infrastructure/mongojs')
const { Subscription } = require('../../../app/src/models/Subscription')
const SubscriptionViewModelBuilder = require('../../../app/src/Features/Subscription/SubscriptionViewModelBuilder')
const MockDocstoreApi = require('./helpers/MockDocstoreApi')
require('./helpers/MockTagsApi')
require('./helpers/MockV1Api')
require('./helpers/MockProjectHistoryApi')
@@ -18,14 +18,12 @@ const request = require('./helpers/request')
const settings = require('settings-sharelatex')
const redis = require('./helpers/redis')
const MockV1Api = require('./helpers/MockV1Api')
const MockTagsApi = require('./helpers/MockTagsApi')
describe('Sessions', function() {
beforeEach(function(done) {
this.timeout(20000)
this.user1 = new User()
this.site_admin = new User({ email: 'admin@example.com' })
MockTagsApi.tags[this.user1] = []
return async.series(
[cb => this.user1.login(cb), cb => this.user1.logout(cb)],
done
@@ -0,0 +1,387 @@
const User = require('./helpers/User')
const async = require('async')
const { expect } = require('chai')
const _ = require('lodash')
const request = require('./helpers/request')
const _initUser = (user, callback) => {
async.series([cb => user.login(cb), cb => user.getCsrfToken(cb)], callback)
}
const _initUsers = (users, callback) => {
async.each(users, _initUser, callback)
}
const _expect200 = (err, response) => {
expect(err).to.not.exist
expect(response.statusCode).to.equal(200)
}
const _expect204 = (err, response) => {
expect(err).to.not.exist
expect(response.statusCode).to.equal(204)
}
const _createTag = (user, name, callback) => {
user.request.post({ url: `/tag`, json: { name: name } }, callback)
}
const _createTags = (user, tagNames, callback) => {
const tags = []
async.series(
tagNames.map(tagName => cb =>
_createTag(user, tagName, (err, response, body) => {
_expect200(err, response)
tags.push(body)
cb()
})
),
err => {
callback(err, tags)
}
)
}
const _getTags = (user, callback) => {
user.request.get({ url: `/tag`, json: true }, callback)
}
const _names = tags => {
return tags.map(tag => tag.name)
}
const _ids = tags => {
return tags.map(tag => tag._id)
}
const _expectTagStructure = tag => {
expect(tag).to.have.keys('_id', 'user_id', 'name', 'project_ids', '__v')
expect(typeof tag._id).to.equal('string')
expect(typeof tag.user_id).to.equal('string')
expect(typeof tag.name).to.equal('string')
expect(tag.project_ids).to.deep.equal([])
}
describe('Tags', function() {
beforeEach(function(done) {
this.user = new User()
this.otherUser = new User()
_initUsers([this.user, this.otherUser], done)
})
describe('get tags, anonymous', function() {
it('should refuse to get user tags', function(done) {
this.user.logout(err => {
if (err) {
return done(err)
}
_getTags(this.user, (err, response, body) => {
expect(err).to.not.exist
expect(response.statusCode).to.equal(302)
expect(body).to.not.exist
done()
})
})
})
})
describe('get tags, none', function() {
it('should get user tags', function(done) {
_getTags(this.user, (err, response, body) => {
_expect200(err, response)
expect(body).to.deep.equal([])
done()
})
})
})
describe('create some tags, then get', function() {
it('should get tags only for that user', function(done) {
// Create a few tags
_createTags(this.user, ['one', 'two', 'three'], (err, tags) => {
expect(err).to.not.exist
// Check structure of tags we just created
expect(tags.length).to.equal(3)
for (let tag of tags) {
_expectTagStructure(tag)
expect(tag.user_id).to.equal(this.user._id.toString())
}
// Get the list of tags for this user
_getTags(this.user, (err, response, body) => {
_expect200(err, response)
expect(body).to.be.an.instanceof(Array)
expect(body.length).to.equal(3)
// Check structure of each tag in response
for (let tag of body) {
_expectTagStructure(tag)
expect(tag.user_id).to.equal(this.user._id.toString())
}
// Check that the set of ids we created are the same as
// the ids we got in the tag-list body
expect(_.sortBy(_ids(tags))).to.deep.equal(_.sortBy(_ids(body)))
// Check that the other user can't see these tags
_getTags(this.otherUser, (err, response, body) => {
_expect200(err, response)
expect(body).to.deep.equal([])
done()
})
})
})
})
})
describe('get tags via api', function() {
const auth = Buffer.from('sharelatex:password').toString('base64')
const authedRequest = request.defaults({
headers: {
Authorization: `Basic ${auth}`
}
})
it('should disallow without appropriate auth headers', function(done) {
_createTags(this.user, ['one', 'two', 'three'], (err, tags) => {
expect(err).to.not.exist
// Get the tags, but with a regular request, not authorized
request.get(
{ url: `/user/${this.user._id}/tag`, json: true },
(err, response, body) => {
expect(err).to.not.exist
expect(response.statusCode).to.equal(401)
expect(body).to.equal('Unauthorized')
done()
}
)
})
})
it('should get the tags from api endpoint', function(done) {
_createTags(this.user, ['one', 'two', 'three'], (err, tags) => {
expect(err).to.not.exist
// Get tags for user
authedRequest.get(
{ url: `/user/${this.user._id}/tag`, json: true },
(err, response, body) => {
_expect200(err, response)
expect(body.length).to.equal(3)
// Get tags for other user, expect none
authedRequest.get(
{ url: `/user/${this.otherUser._id}/tag`, json: true },
(err, response, body) => {
_expect200(err, response)
expect(body.length).to.equal(0)
done()
}
)
}
)
})
})
})
describe('rename tag', function() {
it('should reject malformed tag id', function(done) {
this.user.request.post(
{ url: `/tag/lol/rename`, json: { name: 'five' } },
(err, response) => {
expect(err).to.not.exist
expect(response.statusCode).to.equal(500)
done()
}
)
})
it('should allow user to rename a tag', function(done) {
_createTags(this.user, ['one', 'two'], (err, tags) => {
expect(err).to.not.exist
// Pick out the first tag
const firstTagId = tags[0]._id
// Change its name
this.user.request.post(
{ url: `/tag/${firstTagId}/rename`, json: { name: 'five' } },
(err, response) => {
_expect204(err, response)
// Get the tag list
_getTags(this.user, (err, response, body) => {
_expect200(err, response)
expect(body.length).to.equal(2)
// Check the set of tag names is correct
const tagNames = _names(body)
expect(_.sortBy(tagNames)).to.deep.equal(
_.sortBy(['five', 'two'])
)
// Check the id is the same
const tagWithNameFive = _.find(body, t => t.name === 'five')
expect(tagWithNameFive._id).to.equal(firstTagId)
done()
})
}
)
})
})
it('should not allow other user to change name', function(done) {
const initialTagNames = ['one', 'two']
_createTags(this.user, initialTagNames, (err, tags) => {
expect(err).to.not.exist
const firstTagId = tags[0]._id
// Post with the other user
this.otherUser.request.post(
{ url: `/tag/${firstTagId}/rename`, json: { name: 'six' } },
(err, response) => {
_expect204(err, response)
// Should not have altered the tag
this.user.request.get(
{ url: `/tag`, json: true },
(err, response, body) => {
_expect200(err, response)
expect(_.sortBy(_names(body))).to.deep.equal(
_.sortBy(initialTagNames)
)
done()
}
)
}
)
})
})
})
describe('delete tag', function() {
it('should reject malformed tag id', function(done) {
this.user.request.delete(
{ url: `/tag/lol`, json: { name: 'five' } },
(err, response) => {
expect(err).to.not.exist
expect(response.statusCode).to.equal(500)
done()
}
)
})
it('should delete a tag', function(done) {
const initialTagNames = ['one', 'two', 'three']
_createTags(this.user, initialTagNames, (err, tags) => {
expect(err).to.not.exist
const firstTagId = tags[0]._id
this.user.request.delete(
{ url: `/tag/${firstTagId}` },
(err, response) => {
_expect204(err, response)
// Check the tag list
_getTags(this.user, (err, response, body) => {
_expect200(err, response)
expect(_.sortBy(_names(body))).to.deep.equal(
_.sortBy(['two', 'three'])
)
done()
})
}
)
})
})
})
describe('add project to tag', function() {
beforeEach(function(done) {
this.user.createProject('test 1', (err, projectId) => {
if (err) {
return done(err)
}
this.projectId = projectId
done()
})
})
it('should reject malformed tag id', function(done) {
this.user.request.post(
{ url: `/tag/lol/project/bad` },
(err, response) => {
expect(err).to.not.exist
expect(response.statusCode).to.equal(500)
done()
}
)
})
it('should allow the user to add a project to a tag, and remove it', function(done) {
_createTags(this.user, ['one', 'two'], (err, tags) => {
expect(err).to.not.exist
const firstTagId = tags[0]._id
_getTags(this.user, (err, response, body) => {
_expect200(err, response)
// Confirm that project_ids is empty for this tag
expect(
_.find(body, tag => tag.name === 'one').project_ids
).to.deep.equal([])
// Add the project to the tag
this.user.request.post(
{ url: `/tag/${firstTagId}/project/${this.projectId}` },
(err, response) => {
_expect204(err, response)
// Get tags again
_getTags(this.user, (err, response, body) => {
_expect200(err, response)
// Check the project has been added to project_ids
expect(
_.find(body, tag => tag.name === 'one').project_ids
).to.deep.equal([this.projectId])
// Remove the project from the tag
this.user.request.delete(
{ url: `/tag/${firstTagId}/project/${this.projectId}` },
(err, response) => {
_expect204(err, response)
// Check tag list again
_getTags(this.user, (err, response, body) => {
_expect200(err, response)
// Check the project has been removed from project_ids
expect(
_.find(body, tag => tag.name === 'one').project_ids
).to.deep.equal([])
done()
})
}
)
})
}
)
})
})
})
it('should not allow another user to add a project to the tag', function(done) {
_createTags(this.user, ['one', 'two'], (err, tags) => {
expect(err).to.not.exist
const firstTagId = tags[0]._id
_getTags(this.user, (err, response, body) => {
_expect200(err, response)
// Confirm that project_ids is empty for this tag
expect(
_.find(body, tag => tag.name === 'one').project_ids
).to.deep.equal([])
// Have the other user try to add their own project to the tag
this.otherUser.createProject(
'rogue project',
(err, rogueProjectId) => {
expect(err).to.not.exist
this.otherUser.request.post(
{ url: `/tag/${firstTagId}/project/${rogueProjectId}` },
(err, response) => {
_expect204(err, response)
// Get original user tags again
_getTags(this.user, (err, response, body) => {
_expect200(err, response)
// Check the rogue project has not been added to project_ids
expect(
_.find(body, tag => tag.name === 'one').project_ids
).to.deep.equal([])
done()
})
}
)
}
)
})
})
})
})
})
@@ -1,33 +0,0 @@
const express = require('express')
const app = express()
const MockTagsApi = {
tags: {},
run() {
app.get('/user/:userId/tag', (req, res) => {
const { userId } = req.params
const tags = this.tags[userId]
res.json(tags)
})
app.delete('/user/:user_id/project/:project_id', (req, res) => {
res.sendStatus(200)
})
app
.listen(3012, error => {
if (error) {
throw error
}
})
.on('error', error => {
console.error('error starting MockTagsApi:', error.message)
process.exit(1)
})
}
}
MockTagsApi.run()
module.exports = MockTagsApi
@@ -21,8 +21,8 @@ const modulePath = require('path').join(
)
describe('TagsController', function() {
const user_id = '123nd3ijdks'
const project_id = '123njdskj9jlk'
const userId = '123nd3ijdks'
const projectId = '123njdskj9jlk'
const tag = 'some_class101'
beforeEach(function() {
@@ -54,11 +54,11 @@ describe('TagsController', function() {
})
this.req = {
params: {
project_id
projectId
},
session: {
user: {
_id: user_id
_id: userId
}
}
}
@@ -76,7 +76,7 @@ describe('TagsController', function() {
return this.controller.getAllTags(this.req, {
json: body => {
body.should.equal(allTags)
this.handler.getAllTags.calledWith(user_id).should.equal(true)
this.handler.getAllTags.calledWith(userId).should.equal(true)
return done()
}
})
@@ -86,14 +86,14 @@ describe('TagsController', function() {
describe('createTag', function() {
beforeEach(function() {
this.handler.createTag.callsArgWith(2, null, (this.tag = { mock: 'tag' }))
this.req.session.user._id = this.user_id = 'user-id-123'
this.req.session.user._id = this.userId = 'user-id-123'
this.req.body = { name: (this.name = 'tag-name') }
return this.controller.createTag(this.req, this.res)
})
it('should create the tag in the backend', function() {
return this.handler.createTag
.calledWith(this.user_id, this.name)
.calledWith(this.userId, this.name)
.should.equal(true)
})
@@ -104,14 +104,14 @@ describe('TagsController', function() {
describe('deleteTag', function() {
beforeEach(function() {
this.req.params.tag_id = this.tag_id = 'tag-id-123'
this.req.session.user._id = this.user_id = 'user-id-123'
this.req.params.tagId = this.tagId = 'tag-id-123'
this.req.session.user._id = this.userId = 'user-id-123'
return this.controller.deleteTag(this.req, this.res)
})
it('should delete the tag in the backend', function() {
return this.handler.deleteTag
.calledWith(this.user_id, this.tag_id)
.calledWith(this.userId, this.tagId)
.should.equal(true)
})
@@ -123,8 +123,8 @@ describe('TagsController', function() {
describe('renameTag', function() {
beforeEach(function() {
this.req.params.tag_id = this.tag_id = 'tag-id-123'
return (this.req.session.user._id = this.user_id = 'user-id-123')
this.req.params.tagId = this.tagId = 'tag-id-123'
return (this.req.session.user._id = this.userId = 'user-id-123')
})
describe('with a name', function() {
@@ -135,7 +135,7 @@ describe('TagsController', function() {
it('should delete the tag in the backend', function() {
return this.handler.renameTag
.calledWith(this.user_id, this.tag_id, this.name)
.calledWith(this.userId, this.tagId, this.name)
.should.equal(true)
})
@@ -163,15 +163,15 @@ describe('TagsController', function() {
describe('addProjectToTag', function() {
beforeEach(function() {
this.req.params.tag_id = this.tag_id = 'tag-id-123'
this.req.params.project_id = this.project_id = 'project-id-123'
this.req.session.user._id = this.user_id = 'user-id-123'
this.req.params.tagId = this.tagId = 'tag-id-123'
this.req.params.projectId = this.projectId = 'project-id-123'
this.req.session.user._id = this.userId = 'user-id-123'
return this.controller.addProjectToTag(this.req, this.res)
})
it('should add the tag to the project in the backend', function() {
return this.handler.addProjectToTag
.calledWith(this.user_id, this.tag_id, this.project_id)
.calledWith(this.userId, this.tagId, this.projectId)
.should.equal(true)
})
@@ -183,15 +183,15 @@ describe('TagsController', function() {
describe('removeProjectFromTag', function() {
beforeEach(function() {
this.req.params.tag_id = this.tag_id = 'tag-id-123'
this.req.params.project_id = this.project_id = 'project-id-123'
this.req.session.user._id = this.user_id = 'user-id-123'
this.req.params.tagId = this.tagId = 'tag-id-123'
this.req.params.projectId = this.projectId = 'project-id-123'
this.req.session.user._id = this.userId = 'user-id-123'
return this.controller.removeProjectFromTag(this.req, this.res)
})
it('should remove the tag from the project in the backend', function() {
return this.handler.removeProjectFromTag
.calledWith(this.user_id, this.tag_id, this.project_id)
.calledWith(this.userId, this.tagId, this.projectId)
.should.equal(true)
})
@@ -1,425 +1,260 @@
const SandboxedModule = require('sandboxed-module')
const { assert } = require('chai')
const { expect } = require('chai')
require('chai').should()
const sinon = require('sinon')
const { Tag } = require('../helpers/models/Tag')
const { ObjectId } = require('mongojs')
const modulePath = require('path').join(
__dirname,
'../../../../app/src/Features/Tags/TagsHandler.js'
)
describe('TagsHandler', function() {
const userId = 'user-id-123'
const tagId = 'tag-id-123'
const projectId = 'project-id-123'
const tagsUrl = 'tags.sharelatex.testing'
const tag = 'tag_name'
describe('TagsHandler', function() {
beforeEach(function() {
this.request = {
post: sinon.stub().callsArgWith(1),
del: sinon.stub().callsArgWith(1),
get: sinon.stub()
}
this.userId = ObjectId().toString()
this.callback = sinon.stub()
this.handler = SandboxedModule.require(modulePath, {
globals: {
console: console
},
this.tag = { user_id: this.userId, name: 'some name' }
this.tagId = ObjectId().toString()
this.projectId = ObjectId().toString()
this.mongojs = { ObjectId: ObjectId }
this.TagMock = sinon.mock(Tag)
this.TagsHandler = SandboxedModule.require(modulePath, {
requires: {
'settings-sharelatex': {
apis: { tags: { url: tagsUrl } }
},
request: this.request,
'logger-sharelatex': {
log() {},
warn() {},
err() {}
}
'../../infrastructure/mongojs': this.mongojs,
'../../models/Tag': { Tag: Tag }
}
})
})
describe('removeProjectFromAllTags', function() {
it('should tell the tags api to remove the project_id from all the users tags', function(done) {
this.handler.removeProjectFromAllTags(userId, projectId, () => {
this.request.del
.calledWith({
url: `${tagsUrl}/user/${userId}/project/${projectId}`,
timeout: 10000
})
.should.equal(true)
done()
})
})
})
describe('getAllTags', function() {
it('should get all tags', function(done) {
const stubbedAllTags = [
{ name: 'tag', project_ids: ['123423', '423423'] }
]
this.request.get.callsArgWith(
1,
null,
{ statusCode: 200 },
stubbedAllTags
)
this.handler.getAllTags(userId, (err, allTags) => {
assert.notExists(err)
stubbedAllTags.should.deep.equal(allTags)
const getOpts = {
url: `${tagsUrl}/user/${userId}/tag`,
json: true,
timeout: 10000
}
this.request.get.calledWith(getOpts).should.equal(true)
done()
})
})
it('should callback with an empty array on error', function(done) {
this.request.get.callsArgWith(
1,
{ something: 'wrong' },
{ statusCode: 200 },
[]
)
this.handler.getAllTags(userId, (err, allTags) => {
allTags.length.should.equal(0)
assert.isDefined(err)
done()
})
})
it('should callback with an empty array if there are no tags', function(done) {
this.request.get.callsArgWith(
1,
{ something: 'wrong' },
{ statusCode: 200 },
undefined
)
this.handler.getAllTags(userId, (err, allTags) => {
allTags.length.should.equal(0)
assert.isDefined(err)
done()
})
})
it('should callback with an empty array on a non 200 response', function(done) {
this.request.get.callsArgWith(1, null, { statusCode: 201 }, [])
this.handler.getAllTags(userId, (err, allTags) => {
allTags.length.should.equal(0)
assert.isDefined(err)
done()
})
})
it('should callback with an empty array on no body and no response', function(done) {
this.request.get.callsArgWith(
1,
{ something: 'wrong' },
undefined,
undefined
)
this.handler.getAllTags(userId, (err, allTags) => {
allTags.length.should.equal(0)
assert.isDefined(err)
describe('finding users tags', function() {
it('should find all the documents with that user id', function(done) {
const stubbedTags = [{ name: 'tag1' }, { name: 'tag2' }, { name: 'tag3' }]
this.TagMock.expects('find')
.once()
.withArgs({ user_id: this.userId })
.yields(null, stubbedTags)
this.TagsHandler.getAllTags(this.userId, (err, result) => {
expect(err).to.not.exist
this.TagMock.verify()
expect(result).to.deep.equal(stubbedTags)
done()
})
})
})
describe('createTag', function() {
beforeEach(function() {
this.request.post = sinon
.stub()
.callsArgWith(1, null, { statusCode: 204 }, '')
this.handler.createTag(userId, (this.name = 'tag_name'), this.callback)
})
it('should send a request to the tag backend', function() {
this.request.post
.calledWith({
url: `${tagsUrl}/user/${userId}/tag`,
json: {
name: this.name
},
timeout: 10000
})
.should.equal(true)
})
it('should call the callback with no error', function() {
this.callback.calledWith(null).should.equal(true)
})
})
describe('deleteTag', function() {
describe('successfully', function() {
beforeEach(function() {
this.request.del = sinon
.stub()
.callsArgWith(1, null, { statusCode: 204 }, '')
this.handler.deleteTag(userId, tagId, this.callback)
})
it('should send a request to the tag backend', function() {
this.request.del
.calledWith({
url: `${tagsUrl}/user/${userId}/tag/${tagId}`,
timeout: 10000
})
.should.equal(true)
})
it('should call the callback with no error', function() {
this.callback.calledWith(null).should.equal(true)
})
})
describe('with error', function() {
beforeEach(function() {
this.request.del = sinon
.stub()
.callsArgWith(1, null, { statusCode: 500 }, '')
this.handler.deleteTag(userId, tagId, this.callback)
})
it('should call the callback with an Error', function() {
this.callback
.calledWith(sinon.match.instanceOf(Error))
.should.equal(true)
})
})
})
describe('renameTag', function() {
describe('successfully', function() {
beforeEach(function() {
this.request.post = sinon
.stub()
.callsArgWith(1, null, { statusCode: 204 }, '')
this.handler.renameTag(
userId,
tagId,
(this.name = 'new-name'),
this.callback
describe('when insert succeeds', function() {
it('should call insert in mongo', function(done) {
this.TagMock.expects('create')
.withArgs(this.tag)
.once()
.yields(null, this.tag)
this.TagsHandler.createTag(
this.tag.user_id,
this.tag.name,
(err, resultTag) => {
expect(err).to.not.exist
this.TagMock.verify()
expect(resultTag.user_id).to.equal(this.tag.user_id)
expect(resultTag.name).to.equal(this.tag.name)
done()
}
)
})
it('should send a request to the tag backend', function() {
this.request.post
.calledWith({
url: `${tagsUrl}/user/${userId}/tag/${tagId}/rename`,
json: {
name: this.name
},
timeout: 10000
})
.should.equal(true)
})
it('should call the callback with no error', function() {
this.callback.calledWith(null).should.equal(true)
})
})
describe('with error', function() {
describe('when insert has duplicate key error error', function() {
beforeEach(function() {
this.request.post = sinon
.stub()
.callsArgWith(1, null, { statusCode: 500 }, '')
this.handler.renameTag(userId, tagId, 'name', this.callback)
this.duplicateKeyError = new Error('Duplicate')
this.duplicateKeyError.code = 11000
})
it('should call the callback with an Error', function() {
this.callback
.calledWith(sinon.match.instanceOf(Error))
.should.equal(true)
})
})
})
describe('removeProjectFromTag', function() {
describe('successfully', function() {
beforeEach(function() {
this.request.del = sinon
.stub()
.callsArgWith(1, null, { statusCode: 204 }, '')
this.handler.removeProjectFromTag(
userId,
tagId,
projectId,
this.callback
it('should get tag with findOne and return that tag', function(done) {
this.TagMock.expects('create')
.withArgs(this.tag)
.once()
.yields(this.duplicateKeyError)
this.TagMock.expects('findOne')
.withArgs({ user_id: this.tag.user_id, name: this.tag.name })
.once()
.yields(null, this.tag)
this.TagsHandler.createTag(
this.tag.user_id,
this.tag.name,
(err, resultTag) => {
expect(err).to.not.exist
this.TagMock.verify()
expect(resultTag.user_id).to.equal(this.tag.user_id)
expect(resultTag.name).to.equal(this.tag.name)
done()
}
)
})
it('should send a request to the tag backend', function() {
this.request.del
.calledWith({
url: `${tagsUrl}/user/${userId}/tag/${tagId}/project/${projectId}`,
timeout: 10000
})
.should.equal(true)
})
it('should call the callback with no error', function() {
this.callback.calledWith(null).should.equal(true)
})
})
describe('with error', function() {
beforeEach(function() {
this.request.del = sinon
.stub()
.callsArgWith(1, null, { statusCode: 500 }, '')
this.handler.removeProjectFromTag(
userId,
tagId,
projectId,
this.callback
)
})
it('should call the callback with an Error', function() {
this.callback
.calledWith(sinon.match.instanceOf(Error))
.should.equal(true)
})
})
})
describe('addProjectToTag', function() {
describe('successfully', function() {
beforeEach(function() {
this.request.post = sinon
.stub()
.callsArgWith(1, null, { statusCode: 204 }, '')
this.handler.addProjectToTag(userId, tagId, projectId, this.callback)
})
describe('with a valid tag_id', function() {
beforeEach(function() {})
it('should send a request to the tag backend', function() {
this.request.post
.calledWith({
url: `${tagsUrl}/user/${userId}/tag/${tagId}/project/${projectId}`,
timeout: 10000
})
.should.equal(true)
})
it('should call the callback with no error', function() {
this.callback.calledWith(null).should.equal(true)
})
})
describe('with error', function() {
beforeEach(function() {
this.request.post = sinon
.stub()
.callsArgWith(1, null, { statusCode: 500 }, '')
this.handler.addProjectToTag(userId, tagId, projectId, this.callback)
})
it('should call the callback with an Error', function() {
this.callback
.calledWith(sinon.match.instanceOf(Error))
.should.equal(true)
it('should call update in mongo', function(done) {
this.TagMock.expects('findOneAndUpdate')
.once()
.withArgs(
{ _id: this.tagId, user_id: this.userId },
{ $addToSet: { project_ids: this.projectId } }
)
.yields()
this.TagsHandler.addProjectToTag(
this.userId,
this.tagId,
this.projectId,
err => {
expect(err).to.not.exist
this.TagMock.verify()
done()
}
)
})
})
})
describe('addProjectToTagName', function() {
describe('successfully', function() {
beforeEach(function() {
this.request.post = sinon
.stub()
.callsArgWith(1, null, { statusCode: 204 }, '')
this.handler.addProjectToTagName(userId, tag, projectId, this.callback)
})
it('should send a request to the tag backend', function() {
this.request.post
.calledWith({
json: {
name: tag
},
url: `${tagsUrl}/user/${userId}/tag/project/${projectId}`,
timeout: 10000
})
.should.equal(true)
})
it('should call the callback with no error', function() {
this.callback.calledWith(null).should.equal(true)
})
})
describe('with error', function() {
beforeEach(function() {
this.request.post = sinon
.stub()
.callsArgWith(1, null, { statusCode: 500 }, '')
this.handler.addProjectToTagName(
userId,
tagId,
projectId,
this.callback
it('should call update in mongo', function(done) {
this.TagMock.expects('update')
.once()
.withArgs(
{ name: this.tag.name, user_id: this.tag.userId },
{ $addToSet: { project_ids: this.projectId } },
{ upsert: true }
)
})
it('should call the callback with an Error', function() {
this.callback
.calledWith(sinon.match.instanceOf(Error))
.should.equal(true)
})
.yields()
this.TagsHandler.addProjectToTagName(
this.tag.userId,
this.tag.name,
this.projectId,
err => {
expect(err).to.not.exist
this.TagMock.verify()
done()
}
)
})
})
describe('updateTagUserIds', function() {
describe('successfully', function() {
beforeEach(function() {
this.request.put = sinon
.stub()
.callsArgWith(1, null, { statusCode: 204 }, '')
this.handler.updateTagUserIds(
'old-user-id',
'new-user-id',
this.callback
it('should call update in mongo', function(done) {
this.newUserId = ObjectId().toString()
this.TagMock.expects('update')
.once()
.withArgs(
{ user_id: this.userId },
{ $set: { user_id: this.newUserId } },
{ multi: true }
)
})
it('should send a request to the tag backend', function() {
this.request.put
.calledWith({
json: {
user_id: 'new-user-id'
},
url: `${tagsUrl}/user/old-user-id/tag`,
timeout: 10000
})
.should.equal(true)
})
it('should call the callback with no error', function() {
this.callback.calledWith(null).should.equal(true)
.yields()
this.TagsHandler.updateTagUserIds(this.userId, this.newUserId, err => {
expect(err).to.not.exist
this.TagMock.verify()
done()
})
})
})
describe('with error', function() {
beforeEach(function() {
this.request.put = sinon
.stub()
.callsArgWith(1, null, { statusCode: 500 }, '')
this.handler.updateTagUserIds(
'old-user-id',
'new-user-id',
this.callback
describe('removeProjectFromTag', function() {
describe('with a valid tag_id', function() {
it('should call update in mongo', function(done) {
this.TagMock.expects('update')
.once()
.withArgs(
{
_id: this.tagId,
user_id: this.userId
},
{
$pull: { project_ids: this.projectId }
}
)
.yields()
this.TagsHandler.removeProjectFromTag(
this.userId,
this.tagId,
this.projectId,
err => {
expect(err).to.not.exist
this.TagMock.verify()
done()
}
)
})
})
})
it('should call the callback with an Error', function() {
this.callback
.calledWith(sinon.match.instanceOf(Error))
.should.equal(true)
describe('removeProjectFromAllTags', function() {
it('should pull the project id from the tag', function(done) {
this.TagMock.expects('update')
.once()
.withArgs(
{
user_id: this.userId
},
{
$pull: { project_ids: this.projectId }
}
)
.yields()
this.TagsHandler.removeProjectFromAllTags(
this.userId,
this.projectId,
err => {
expect(err).to.not.exist
this.TagMock.verify()
done()
}
)
})
})
describe('deleteTag', function() {
describe('with a valid tag_id', function() {
it('should call remove in mongo', function(done) {
this.TagMock.expects('remove')
.once()
.withArgs({ _id: this.tagId, user_id: this.userId })
.yields()
this.TagsHandler.deleteTag(this.userId, this.tagId, err => {
expect(err).to.not.exist
this.TagMock.verify()
done()
})
})
})
})
describe('renameTag', function() {
describe('with a valid tag_id', function() {
it('should call remove in mongo', function(done) {
this.newName = 'new name'
this.TagMock.expects('update')
.once()
.withArgs(
{ _id: this.tagId, user_id: this.userId },
{ $set: { name: this.newName } }
)
.yields()
this.TagsHandler.renameTag(
this.userId,
this.tagId,
this.newName,
err => {
expect(err).to.not.exist
this.TagMock.verify()
done()
}
)
})
})
})
@@ -0,0 +1,3 @@
const mockModel = require('../MockModel')
module.exports = mockModel('Tag')