Merge pull request #3638 from overleaf/em-dropbox-duplicate-projects

Unlink Dropbox when two projects have the same name

GitOrigin-RevId: b16dbeb6841eaebd8553884eebc87e681d17a9c8
This commit is contained in:
Eric Mc Sween
2021-02-18 03:05:12 +00:00
committed by Copybot
parent c79d9ce8c5
commit 1c0b897835
15 changed files with 1228 additions and 1417 deletions
@@ -168,8 +168,6 @@ class InvalidQueryError extends OErrorV2CompatibleError {
}
}
class ProjectIsArchivedOrTrashedError extends BackwardCompatibleError {}
class AffiliationError extends OError {}
module.exports = {
@@ -201,6 +199,5 @@ module.exports = {
UserNotCollaboratorError,
DocHasRangesError,
InvalidQueryError,
ProjectIsArchivedOrTrashedError,
AffiliationError
}
@@ -1,20 +1,34 @@
/* 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 NotificationsHandler = require('./NotificationsHandler')
const { promisifyAll } = require('../../util/promises')
const request = require('request')
const settings = require('settings-sharelatex')
function dropboxDuplicateProjectNames(userId) {
return {
key: `dropboxDuplicateProjectNames-${userId}`,
create(projectName, callback) {
if (callback == null) {
callback = function() {}
}
NotificationsHandler.createNotification(
userId,
this.key,
'notification_dropbox_duplicate_project_names',
{ projectName },
null,
true,
callback
)
},
read(callback) {
if (callback == null) {
callback = function() {}
}
NotificationsHandler.markAsReadWithKey(userId, this.key, callback)
}
}
}
function featuresUpgradedByAffiliation(affiliation, user) {
return {
key: `features-updated-by=${affiliation.institutionId}`,
@@ -23,7 +37,7 @@ function featuresUpgradedByAffiliation(affiliation, user) {
callback = function() {}
}
const messageOpts = { institutionName: affiliation.institutionName }
return NotificationsHandler.createNotification(
NotificationsHandler.createNotification(
user._id,
this.key,
'notification_features_upgraded_by_affiliation',
@@ -37,7 +51,7 @@ function featuresUpgradedByAffiliation(affiliation, user) {
if (callback == null) {
callback = function() {}
}
return NotificationsHandler.markAsReadByKeyOnly(this.key, callback)
NotificationsHandler.markAsReadByKeyOnly(this.key, callback)
}
}
}
@@ -50,7 +64,7 @@ function redundantPersonalSubscription(affiliation, user) {
callback = function() {}
}
const messageOpts = { institutionName: affiliation.institutionName }
return NotificationsHandler.createNotification(
NotificationsHandler.createNotification(
user._id,
this.key,
'notification_personal_subscription_not_required_due_to_affiliation',
@@ -64,7 +78,7 @@ function redundantPersonalSubscription(affiliation, user) {
if (callback == null) {
callback = function() {}
}
return NotificationsHandler.markAsReadByKeyOnly(this.key, callback)
NotificationsHandler.markAsReadByKeyOnly(this.key, callback)
}
}
}
@@ -82,7 +96,7 @@ function projectInvite(invite, project, sendingUser, user) {
projectId: project._id.toString(),
token: invite.token
}
return NotificationsHandler.createNotification(
NotificationsHandler.createNotification(
user._id,
this.key,
'notification_project_invite',
@@ -95,7 +109,7 @@ function projectInvite(invite, project, sendingUser, user) {
if (callback == null) {
callback = function() {}
}
return NotificationsHandler.markAsReadByKeyOnly(this.key, callback)
NotificationsHandler.markAsReadByKeyOnly(this.key, callback)
}
}
}
@@ -107,9 +121,10 @@ function ipMatcherAffiliation(userId) {
callback = function() {}
}
if (!settings.apis.v1.url) {
return null
} // service is not configured
return request(
// service is not configured
return callback()
}
request(
{
method: 'GET',
url: `${settings.apis.v1.url}/api/v2/users/${userId}/ip_matcher`,
@@ -120,10 +135,10 @@ function ipMatcherAffiliation(userId) {
},
function(error, response, body) {
if (error != null) {
return error
return callback(error)
}
if (response.statusCode !== 200) {
return null
return callback()
}
const key = `ip-matched-affiliation-${body.id}`
@@ -137,7 +152,7 @@ function ipMatcherAffiliation(userId) {
portalPath,
ssoEnabled: body.sso_enabled
}
return NotificationsHandler.createNotification(
NotificationsHandler.createNotification(
userId,
key,
'notification_ip_matched_affiliation',
@@ -150,19 +165,19 @@ function ipMatcherAffiliation(userId) {
)
},
read(university_id, callback) {
read(universityId, callback) {
if (callback == null) {
callback = function() {}
}
const key = `ip-matched-affiliation-${university_id}`
return NotificationsHandler.markAsReadWithKey(userId, key, callback)
const key = `ip-matched-affiliation-${universityId}`
NotificationsHandler.markAsReadWithKey(userId, key, callback)
}
}
}
function tpdsFileLimit(user_id) {
function tpdsFileLimit(userId) {
return {
key: `tpdsFileLimit-${user_id}`,
key: `tpdsFileLimit-${userId}`,
create(projectName, callback) {
if (callback == null) {
callback = function() {}
@@ -170,8 +185,8 @@ function tpdsFileLimit(user_id) {
const messageOpts = {
projectName: projectName
}
return NotificationsHandler.createNotification(
user_id,
NotificationsHandler.createNotification(
userId,
this.key,
'notification_tpds_file_limit',
messageOpts,
@@ -184,22 +199,18 @@ function tpdsFileLimit(user_id) {
if (callback == null) {
callback = function() {}
}
return NotificationsHandler.markAsReadByKeyOnly(this.key, callback)
NotificationsHandler.markAsReadByKeyOnly(this.key, callback)
}
}
}
const NotificationsBuilder = {
// Note: notification keys should be url-safe
dropboxDuplicateProjectNames,
featuresUpgradedByAffiliation,
redundantPersonalSubscription,
projectInvite,
ipMatcherAffiliation,
tpdsFileLimit
}
@@ -1,23 +1,7 @@
/* eslint-disable
camelcase,
node/handle-callback-err,
max-len,
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
* DS202: Simplify dynamic range loops
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const { db } = require('../../infrastructure/mongodb')
const { normalizeQuery } = require('../Helpers/Mongo')
const OError = require('@overleaf/o-error')
const metrics = require('@overleaf/metrics')
const async = require('async')
const { promisifyAll } = require('../../util/promises')
const { Project } = require('../../models/Project')
const logger = require('logger-sharelatex')
@@ -27,76 +11,55 @@ const { DeletedProject } = require('../../models/DeletedProject')
const ProjectGetter = {
EXCLUDE_DEPTH: 8,
getProjectWithoutDocLines(project_id, callback) {
if (callback == null) {
callback = function(error, project) {}
}
getProjectWithoutDocLines(projectId, callback) {
const excludes = {}
for (
let i = 1, end = ProjectGetter.EXCLUDE_DEPTH, asc = end >= 1;
asc ? i <= end : i >= end;
asc ? i++ : i--
) {
for (let i = 1; i <= ProjectGetter.EXCLUDE_DEPTH; i++) {
excludes[`rootFolder${Array(i).join('.folders')}.docs.lines`] = 0
}
return ProjectGetter.getProject(project_id, excludes, callback)
ProjectGetter.getProject(projectId, excludes, callback)
},
getProjectWithOnlyFolders(project_id, callback) {
if (callback == null) {
callback = function(error, project) {}
}
getProjectWithOnlyFolders(projectId, callback) {
const excludes = {}
for (
let i = 1, end = ProjectGetter.EXCLUDE_DEPTH, asc = end >= 1;
asc ? i <= end : i >= end;
asc ? i++ : i--
) {
for (let i = 1; i <= ProjectGetter.EXCLUDE_DEPTH; i++) {
excludes[`rootFolder${Array(i).join('.folders')}.docs`] = 0
excludes[`rootFolder${Array(i).join('.folders')}.fileRefs`] = 0
}
return ProjectGetter.getProject(project_id, excludes, callback)
ProjectGetter.getProject(projectId, excludes, callback)
},
getProject(project_id, projection, callback) {
getProject(projectId, projection, callback) {
if (typeof projection === 'function' && callback == null) {
callback = projection
projection = {}
}
if (project_id == null) {
return callback(new Error('no project_id provided'))
if (projectId == null) {
return callback(new Error('no project id provided'))
}
if (typeof projection !== 'object') {
return callback(new Error('projection is not an object'))
}
if (
(projection != null ? projection.rootFolder : undefined) ||
Object.keys(projection).length === 0
) {
if (projection.rootFolder || Object.keys(projection).length === 0) {
const ProjectEntityMongoUpdateHandler = require('./ProjectEntityMongoUpdateHandler')
return LockManager.runWithLock(
LockManager.runWithLock(
ProjectEntityMongoUpdateHandler.LOCK_NAMESPACE,
project_id,
cb => ProjectGetter.getProjectWithoutLock(project_id, projection, cb),
projectId,
cb => ProjectGetter.getProjectWithoutLock(projectId, projection, cb),
callback
)
} else {
return ProjectGetter.getProjectWithoutLock(
project_id,
projection,
callback
)
ProjectGetter.getProjectWithoutLock(projectId, projection, callback)
}
},
getProjectWithoutLock(project_id, projection, callback) {
getProjectWithoutLock(projectId, projection, callback) {
if (typeof projection === 'function' && callback == null) {
callback = projection
projection = {}
}
if (project_id == null) {
return callback(new Error('no project_id provided'))
if (projectId == null) {
return callback(new Error('no project id provided'))
}
if (typeof projection !== 'object') {
return callback(new Error('projection is not an object'))
@@ -104,111 +67,98 @@ const ProjectGetter = {
let query
try {
query = normalizeQuery(project_id)
query = normalizeQuery(projectId)
} catch (err) {
return callback(err)
}
return db.projects.findOne(query, { projection }, function(err, project) {
if (err != null) {
db.projects.findOne(query, { projection }, function(err, project) {
if (err) {
OError.tag(err, 'error getting project', {
query,
projection
})
return callback(err)
}
return callback(null, project)
callback(null, project)
})
},
getProjectIdByReadAndWriteToken(token, callback) {
if (callback == null) {
callback = function(err, project_id) {}
}
return Project.findOne(
{ 'tokens.readAndWrite': token },
{ _id: 1 },
function(err, project) {
if (err != null) {
return callback(err)
}
if (project == null) {
return callback()
}
return callback(null, project._id)
}
)
},
getProjectByV1Id(v1_id, callback) {
if (callback == null) {
callback = function(err, v1_id) {}
}
return Project.findOne({ 'overleaf.id': v1_id }, { _id: 1 }, function(
Project.findOne({ 'tokens.readAndWrite': token }, { _id: 1 }, function(
err,
project
) {
if (err != null) {
if (err) {
return callback(err)
}
if (project == null) {
return callback()
}
return callback(null, project._id)
callback(null, project._id)
})
},
findAllUsersProjects(user_id, fields, callback) {
if (callback == null) {
callback = function(error, projects) {
if (projects == null) {
projects = {
owned: [],
readAndWrite: [],
readOnly: [],
tokenReadAndWrite: [],
tokenReadOnly: []
}
}
}
}
findAllUsersProjects(userId, fields, callback) {
const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
return Project.find({ owner_ref: user_id }, fields, function(
error,
ownedProjects
) {
if (error != null) {
Project.find({ owner_ref: userId }, fields, function(error, ownedProjects) {
if (error) {
return callback(error)
}
return CollaboratorsGetter.getProjectsUserIsMemberOf(
user_id,
fields,
function(error, projects) {
if (error != null) {
return callback(error)
}
const result = {
owned: ownedProjects || [],
readAndWrite: projects.readAndWrite || [],
readOnly: projects.readOnly || [],
tokenReadAndWrite: projects.tokenReadAndWrite || [],
tokenReadOnly: projects.tokenReadOnly || []
}
return callback(null, result)
CollaboratorsGetter.getProjectsUserIsMemberOf(userId, fields, function(
error,
projects
) {
if (error) {
return callback(error)
}
)
const result = {
owned: ownedProjects || [],
readAndWrite: projects.readAndWrite || [],
readOnly: projects.readOnly || [],
tokenReadAndWrite: projects.tokenReadAndWrite || [],
tokenReadOnly: projects.tokenReadOnly || []
}
callback(null, result)
})
})
},
getUsersDeletedProjects(user_id, callback) {
/**
* Return all projects with the given name that belong to the given user.
*
* Projects include the user's own projects as well as collaborations with
* read/write access.
*/
findUsersProjectsByName(userId, projectName, callback) {
ProjectGetter.findAllUsersProjects(
userId,
'name archived trashed',
(err, allProjects) => {
if (err != null) {
return callback(err)
}
const { owned, readAndWrite } = allProjects
const projects = owned.concat(readAndWrite)
const lowerCasedProjectName = projectName.toLowerCase()
const matches = projects.filter(
project => project.name.toLowerCase() === lowerCasedProjectName
)
callback(null, matches)
}
)
},
getUsersDeletedProjects(userId, callback) {
DeletedProject.find(
{
'deleterData.deletedProjectOwnerId': user_id
'deleterData.deletedProjectOwnerId': userId
},
callback
)
}
}
;['getProject', 'getProjectWithoutDocLines'].map(method =>
metrics.timeAsyncMethod(ProjectGetter, method, 'mongo.ProjectGetter', logger)
)
@@ -1,316 +1,259 @@
const ProjectGetter = require('./ProjectGetter')
const ProjectHelper = require('./ProjectHelper')
const Errors = require('../Errors/Errors')
const _ = require('underscore')
const logger = require('logger-sharelatex')
const async = require('async')
const { promisifyAll } = require('../../util/promises')
const ProjectGetter = require('./ProjectGetter')
const Errors = require('../Errors/Errors')
const { promisifyMultiResult } = require('../../util/promises')
const ProjectLocator = {
findElement(options, _callback) {
// The search algorithm below potentially invokes the callback multiple
// times.
const callback = _.once(_callback)
function findElement(options, _callback) {
// The search algorithm below potentially invokes the callback multiple
// times.
const callback = _.once(_callback)
const {
project,
project_id: projectId,
element_id: elementId,
type
} = options
const elementType = sanitizeTypeOfElement(type)
const {
project,
project_id: projectId,
element_id: elementId,
type
} = options
const elementType = sanitizeTypeOfElement(type)
let count = 0
const endOfBranch = function() {
if (--count === 0) {
logger.warn(
`element ${elementId} could not be found for project ${projectId ||
project._id}`
)
callback(new Errors.NotFoundError('entity not found'))
}
let count = 0
const endOfBranch = function() {
if (--count === 0) {
logger.warn(
`element ${elementId} could not be found for project ${projectId ||
project._id}`
)
callback(new Errors.NotFoundError('entity not found'))
}
}
function search(searchFolder, path) {
count++
const element = _.find(
function search(searchFolder, path) {
count++
const element = _.find(
searchFolder[elementType],
el => (el != null ? el._id : undefined) + '' === elementId + ''
) // need to ToString both id's for robustness
if (
element == null &&
searchFolder.folders != null &&
searchFolder.folders.length !== 0
) {
_.each(searchFolder.folders, (folder, index) => {
if (folder == null) {
return
}
const newPath = {}
for (let key of Object.keys(path)) {
const value = path[key]
newPath[key] = value
} // make a value copy of the string
newPath.fileSystem += `/${folder.name}`
newPath.mongo += `.folders.${index}`
search(folder, newPath)
})
endOfBranch()
} else if (element != null) {
const elementPlaceInArray = getIndexOf(
searchFolder[elementType],
el => (el != null ? el._id : undefined) + '' === elementId + ''
) // need to ToString both id's for robustness
if (
element == null &&
searchFolder.folders != null &&
searchFolder.folders.length !== 0
) {
_.each(searchFolder.folders, (folder, index) => {
if (folder == null) {
return
}
const newPath = {}
for (let key of Object.keys(path)) {
const value = path[key]
newPath[key] = value
} // make a value copy of the string
newPath.fileSystem += `/${folder.name}`
newPath.mongo += `.folders.${index}`
search(folder, newPath)
})
endOfBranch()
} else if (element != null) {
const elementPlaceInArray = getIndexOf(
searchFolder[elementType],
elementId
)
path.fileSystem += `/${element.name}`
path.mongo += `.${elementType}.${elementPlaceInArray}`
callback(null, element, path, searchFolder)
} else if (element == null) {
endOfBranch()
}
}
const path = { fileSystem: '', mongo: 'rootFolder.0' }
const startSearch = project => {
if (elementId + '' === project.rootFolder[0]._id + '') {
callback(null, project.rootFolder[0], path, null)
} else {
search(project.rootFolder[0], path)
}
}
if (project != null) {
startSearch(project)
} else {
ProjectGetter.getProject(
projectId,
{ rootFolder: true, rootDoc_id: true },
(err, project) => {
if (err != null) {
return callback(err)
}
if (project == null) {
return callback(new Errors.NotFoundError('project not found'))
}
startSearch(project)
}
elementId
)
path.fileSystem += `/${element.name}`
path.mongo += `.${elementType}.${elementPlaceInArray}`
callback(null, element, path, searchFolder)
} else if (element == null) {
endOfBranch()
}
},
}
findRootDoc(opts, callback) {
const getRootDoc = project => {
if (project.rootDoc_id != null) {
ProjectLocator.findElement(
{ project, element_id: project.rootDoc_id, type: 'docs' },
(error, ...args) => {
if (error != null) {
if (error instanceof Errors.NotFoundError) {
return callback(null, null)
} else {
return callback(error)
}
}
callback(null, ...args)
}
)
} else {
callback(null, null)
}
}
const { project, project_id: projectId } = opts
if (project != null) {
getRootDoc(project)
const path = { fileSystem: '', mongo: 'rootFolder.0' }
const startSearch = project => {
if (elementId + '' === project.rootFolder[0]._id + '') {
callback(null, project.rootFolder[0], path, null)
} else {
ProjectGetter.getProject(
projectId,
{ rootFolder: true, rootDoc_id: true },
(err, project) => {
if (err != null) {
logger.warn({ err }, 'error getting project')
callback(err)
} else {
getRootDoc(project)
}
}
)
search(project.rootFolder[0], path)
}
},
}
findElementByPath(options, callback) {
const { project, project_id: projectId, path, exactCaseMatch } = options
if (path == null) {
return new Error('no path provided for findElementByPath')
}
if (project != null) {
ProjectLocator._findElementByPathWithProject(
project,
path,
exactCaseMatch,
callback
)
} else {
ProjectGetter.getProject(
projectId,
{ rootFolder: true, rootDoc_id: true },
(err, project) => {
if (err != null) {
return callback(err)
}
ProjectLocator._findElementByPathWithProject(
project,
path,
exactCaseMatch,
callback
)
}
)
}
},
_findElementByPathWithProject(project, needlePath, exactCaseMatch, callback) {
let matchFn
if (exactCaseMatch) {
matchFn = (a, b) => a === b
} else {
matchFn = (a, b) =>
(a != null ? a.toLowerCase() : undefined) ===
(b != null ? b.toLowerCase() : undefined)
}
function getParentFolder(haystackFolder, foldersList, level, cb) {
if (foldersList.length === 0) {
return cb(null, haystackFolder)
}
const needleFolderName = foldersList[level]
let found = false
for (let folder of haystackFolder.folders) {
if (matchFn(folder.name, needleFolderName)) {
found = true
if (level === foldersList.length - 1) {
return cb(null, folder)
} else {
return getParentFolder(folder, foldersList, level + 1, cb)
}
}
}
if (!found) {
cb(
new Error(
`not found project: ${project._id} search path: ${needlePath}, folder ${foldersList[level]} could not be found`
)
)
}
}
function getEntity(folder, entityName, cb) {
let result, type
if (entityName == null) {
return cb(null, folder, 'folder')
}
for (let file of folder.fileRefs || []) {
if (matchFn(file != null ? file.name : undefined, entityName)) {
result = file
type = 'file'
}
}
for (let doc of folder.docs || []) {
if (matchFn(doc != null ? doc.name : undefined, entityName)) {
result = doc
type = 'doc'
}
}
for (let childFolder of folder.folders || []) {
if (
matchFn(
childFolder != null ? childFolder.name : undefined,
entityName
)
) {
result = childFolder
type = 'folder'
}
}
if (result != null) {
cb(null, result, type)
} else {
cb(
new Error(
`not found project: ${project._id} search path: ${needlePath}, entity ${entityName} could not be found`
)
)
}
}
if (project == null) {
return callback(new Error('Tried to find an element for a null project'))
}
if (needlePath === '' || needlePath === '/') {
return callback(null, project.rootFolder[0], 'folder')
}
if (needlePath.indexOf('/') === 0) {
needlePath = needlePath.substring(1)
}
const foldersList = needlePath.split('/')
const needleName = foldersList.pop()
const rootFolder = project.rootFolder[0]
const jobs = []
jobs.push(cb => getParentFolder(rootFolder, foldersList, 0, cb))
jobs.push((folder, cb) => getEntity(folder, needleName, cb))
async.waterfall(jobs, callback)
},
findUsersProjectByName(userId, projectName, callback) {
ProjectGetter.findAllUsersProjects(
userId,
'name archived trashed',
(err, allProjects) => {
if (project != null) {
startSearch(project)
} else {
ProjectGetter.getProject(
projectId,
{ rootFolder: true, rootDoc_id: true },
(err, project) => {
if (err != null) {
return callback(err)
}
const { owned, readAndWrite } = allProjects
const projects = owned.concat(readAndWrite)
projectName = projectName.toLowerCase()
const _findNonArchivedProject = () =>
_.find(
projects,
project =>
project.name.toLowerCase() === projectName &&
!ProjectHelper.isArchivedOrTrashed(project, userId)
)
const _findArchivedProject = () =>
_.find(
projects,
project =>
project.name.toLowerCase() === projectName &&
ProjectHelper.isArchivedOrTrashed(project, userId)
)
const nonArchivedProject = _findNonArchivedProject()
if (nonArchivedProject) {
return callback(null, {
project: nonArchivedProject,
isArchivedOrTrashed: false
})
} else {
const archivedProject = _findArchivedProject()
if (archivedProject) {
return callback(null, {
project: archivedProject,
isArchivedOrTrashed: true
})
} else {
return callback(null, { project: null })
if (project == null) {
return callback(new Errors.NotFoundError('project not found'))
}
startSearch(project)
}
)
}
}
function findRootDoc(opts, callback) {
const getRootDoc = project => {
if (project.rootDoc_id != null) {
findElement(
{ project, element_id: project.rootDoc_id, type: 'docs' },
(error, ...args) => {
if (error != null) {
if (error instanceof Errors.NotFoundError) {
return callback(null, null)
} else {
return callback(error)
}
}
callback(null, ...args)
}
)
} else {
callback(null, null)
}
}
const { project, project_id: projectId } = opts
if (project != null) {
getRootDoc(project)
} else {
ProjectGetter.getProject(
projectId,
{ rootFolder: true, rootDoc_id: true },
(err, project) => {
if (err != null) {
logger.warn({ err }, 'error getting project')
callback(err)
} else {
getRootDoc(project)
}
}
)
}
}
function findElementByPath(options, callback) {
const { project, project_id: projectId, path, exactCaseMatch } = options
if (path == null) {
return new Error('no path provided for findElementByPath')
}
if (project != null) {
_findElementByPathWithProject(project, path, exactCaseMatch, callback)
} else {
ProjectGetter.getProject(
projectId,
{ rootFolder: true, rootDoc_id: true },
(err, project) => {
if (err != null) {
return callback(err)
}
_findElementByPathWithProject(project, path, exactCaseMatch, callback)
}
)
}
}
function _findElementByPathWithProject(
project,
needlePath,
exactCaseMatch,
callback
) {
let matchFn
if (exactCaseMatch) {
matchFn = (a, b) => a === b
} else {
matchFn = (a, b) =>
(a != null ? a.toLowerCase() : undefined) ===
(b != null ? b.toLowerCase() : undefined)
}
function getParentFolder(haystackFolder, foldersList, level, cb) {
if (foldersList.length === 0) {
return cb(null, haystackFolder)
}
const needleFolderName = foldersList[level]
let found = false
for (let folder of haystackFolder.folders) {
if (matchFn(folder.name, needleFolderName)) {
found = true
if (level === foldersList.length - 1) {
return cb(null, folder)
} else {
return getParentFolder(folder, foldersList, level + 1, cb)
}
}
}
if (!found) {
cb(
new Error(
`not found project: ${project._id} search path: ${needlePath}, folder ${foldersList[level]} could not be found`
)
)
}
}
function getEntity(folder, entityName, cb) {
let result, type
if (entityName == null) {
return cb(null, folder, 'folder')
}
for (let file of folder.fileRefs || []) {
if (matchFn(file != null ? file.name : undefined, entityName)) {
result = file
type = 'file'
}
}
for (let doc of folder.docs || []) {
if (matchFn(doc != null ? doc.name : undefined, entityName)) {
result = doc
type = 'doc'
}
}
for (let childFolder of folder.folders || []) {
if (
matchFn(childFolder != null ? childFolder.name : undefined, entityName)
) {
result = childFolder
type = 'folder'
}
}
if (result != null) {
cb(null, result, type)
} else {
cb(
new Error(
`not found project: ${project._id} search path: ${needlePath}, entity ${entityName} could not be found`
)
)
}
}
if (project == null) {
return callback(new Error('Tried to find an element for a null project'))
}
if (needlePath === '' || needlePath === '/') {
return callback(null, project.rootFolder[0], 'folder')
}
if (needlePath.indexOf('/') === 0) {
needlePath = needlePath.substring(1)
}
const foldersList = needlePath.split('/')
const needleName = foldersList.pop()
const rootFolder = project.rootFolder[0]
const jobs = []
jobs.push(cb => getParentFolder(rootFolder, foldersList, 0, cb))
jobs.push((folder, cb) => getEntity(folder, needleName, cb))
async.waterfall(jobs, callback)
}
function sanitizeTypeOfElement(elementType) {
const lastChar = elementType.slice(-1)
if (lastChar !== 's') {
@@ -337,11 +280,24 @@ function getIndexOf(searchEntity, id) {
}
}
module.exports = ProjectLocator
module.exports.promises = promisifyAll(ProjectLocator, {
multiResult: {
findElement: ['element', 'path', 'folder'],
findElementByPath: ['element', 'type'],
findRootDoc: ['element', 'path', 'folder']
module.exports = {
findElement,
findElementByPath,
findRootDoc,
promises: {
findElement: promisifyMultiResult(findElement, [
'element',
'path',
'folder'
]),
findElementByPath: promisifyMultiResult(findElementByPath, [
'element',
'type'
]),
findRootDoc: promisifyMultiResult(findRootDoc, [
'element',
'path',
'folder'
])
}
})
}
@@ -30,7 +30,11 @@ const FeaturesUpdater = {
if (oldFeatures.dropbox === true && features.dropbox === false) {
logger.log({ userId }, '[FeaturesUpdater] must unlink dropbox')
const Modules = require('../../infrastructure/Modules')
Modules.hooks.fire('removeDropbox', userId, () => {})
Modules.hooks.fire('removeDropbox', userId, err => {
if (err) {
logger.error(err)
}
})
}
return callback()
})
@@ -1,19 +1,6 @@
/* eslint-disable
camelcase,
node/handle-callback-err,
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
*/
let parseParams
const tpdsUpdateHandler = require('./TpdsUpdateHandler')
const TpdsUpdateHandler = require('./TpdsUpdateHandler')
const UpdateMerger = require('./UpdateMerger')
const Errors = require('../Errors/Errors')
const logger = require('logger-sharelatex')
@@ -29,45 +16,39 @@ module.exports = {
// They also ignore 'noisy' files like .DS_Store, .gitignore, etc.
mergeUpdate(req, res) {
metrics.inc('tpds.merge-update')
const { filePath, user_id, projectName } = parseParams(req)
const { filePath, userId, projectName } = parseParams(req)
const source = req.headers['x-sl-update-source'] || 'unknown'
return tpdsUpdateHandler.newUpdate(
user_id,
TpdsUpdateHandler.newUpdate(
userId,
projectName,
filePath,
req,
source,
function(err) {
if (err != null) {
err => {
if (err) {
if (err.name === 'TooManyRequestsError') {
logger.warn(
{ err, user_id, filePath },
{ err, userId, filePath },
'tpds update failed to be processed, too many requests'
)
return res.sendStatus(429)
} else if (err.name === 'ProjectIsArchivedOrTrashedError') {
logger.info(
{ err, user_id, filePath, projectName },
'tpds project is archived'
)
return res.sendStatus(409)
res.sendStatus(429)
} else if (err.message === 'project_has_too_many_files') {
logger.warn(
{ err, user_id, filePath },
{ err, userId, filePath },
'tpds trying to append to project over file limit'
)
NotificationsBuilder.tpdsFileLimit(user_id).create(projectName)
return res.sendStatus(400)
NotificationsBuilder.tpdsFileLimit(userId).create(projectName)
res.sendStatus(400)
} else {
logger.err(
{ err, user_id, filePath },
'error reciving update from tpds'
{ err, userId, filePath },
'error receiving update from tpds'
)
return res.sendStatus(500)
res.sendStatus(500)
}
} else {
return res.sendStatus(200)
res.sendStatus(200)
}
}
)
@@ -75,22 +56,22 @@ module.exports = {
deleteUpdate(req, res) {
metrics.inc('tpds.delete-update')
const { filePath, user_id, projectName } = parseParams(req)
const { filePath, userId, projectName } = parseParams(req)
const source = req.headers['x-sl-update-source'] || 'unknown'
return tpdsUpdateHandler.deleteUpdate(
user_id,
TpdsUpdateHandler.deleteUpdate(
userId,
projectName,
filePath,
source,
function(err) {
if (err != null) {
err => {
if (err) {
logger.err(
{ err, user_id, filePath },
'error reciving update from tpds'
{ err, userId, filePath },
'error receiving update from tpds'
)
return res.sendStatus(500)
res.sendStatus(500)
} else {
return res.sendStatus(200)
res.sendStatus(200)
}
}
)
@@ -101,46 +82,31 @@ module.exports = {
// files like .DS_Store, .gitignore, etc because people are generally more explicit with the files they
// want in git.
updateProjectContents(req, res, next) {
if (next == null) {
next = function(error) {}
}
const { project_id } = req.params
const projectId = req.params.project_id
const path = `/${req.params[0]}` // UpdateMerger expects leading slash
const source = req.headers['x-sl-update-source'] || 'unknown'
return UpdateMerger.mergeUpdate(
null,
project_id,
path,
req,
source,
function(error) {
if (error != null) {
if (error.constructor === Errors.InvalidNameError) {
return res.sendStatus(422)
} else {
return next(error)
}
UpdateMerger.mergeUpdate(null, projectId, path, req, source, error => {
if (error) {
if (error.constructor === Errors.InvalidNameError) {
return res.sendStatus(422)
} else {
return next(error)
}
return res.sendStatus(200)
}
)
res.sendStatus(200)
})
},
deleteProjectContents(req, res, next) {
if (next == null) {
next = function(error) {}
}
const { project_id } = req.params
const projectId = req.params.project_id
const path = `/${req.params[0]}` // UpdateMerger expects leading slash
const source = req.headers['x-sl-update-source'] || 'unknown'
return UpdateMerger.deleteUpdate(null, project_id, path, source, function(
error
) {
if (error != null) {
UpdateMerger.deleteUpdate(null, projectId, path, source, error => {
if (error) {
return next(error)
}
return res.sendStatus(200)
res.sendStatus(200)
})
},
@@ -156,7 +122,7 @@ module.exports = {
parseParams: (parseParams = function(req) {
let filePath, projectName
let path = req.params[0]
const { user_id } = req.params
const userId = req.params.user_id
path = Path.join('/', path)
if (path.substring(1).indexOf('/') === -1) {
@@ -168,6 +134,6 @@ module.exports = {
projectName = projectName.replace('/', '')
}
return { filePath, user_id, projectName }
return { filePath, userId, projectName }
})
}
@@ -1,76 +1,30 @@
/* eslint-disable
camelcase,
node/handle-callback-err,
max-len,
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
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const updateMerger = require('./UpdateMerger')
const UpdateMerger = require('./UpdateMerger')
const logger = require('logger-sharelatex')
const projectLocator = require('../Project/ProjectLocator')
const projectCreationHandler = require('../Project/ProjectCreationHandler')
const projectDeleter = require('../Project/ProjectDeleter')
const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
const ProjectCreationHandler = require('../Project/ProjectCreationHandler')
const ProjectDeleter = require('../Project/ProjectDeleter')
const ProjectGetter = require('../Project/ProjectGetter')
const ProjectHelper = require('../Project/ProjectHelper')
const ProjectRootDocManager = require('../Project/ProjectRootDocManager')
const FileTypeManager = require('../Uploads/FileTypeManager')
const CooldownManager = require('../Cooldown/CooldownManager')
const Errors = require('../Errors/Errors')
const Modules = require('../../infrastructure/Modules')
const commitMessage = 'Before update from Dropbox'
const ROOT_DOC_TIMEOUT_LENGTH = 30 * 1000
module.exports = {
newUpdate(user_id, projectName, path, updateRequest, source, callback) {
const getOrCreateProject = cb => {
return projectLocator.findUsersProjectByName(
user_id,
projectName,
(err, result) => {
if (err) {
return callback(err)
}
if (!result) {
return callback(new Error('no data from project locator'))
}
const { project, isArchivedOrTrashed } = result
if (project == null) {
return projectCreationHandler.createBlankProject(
user_id,
projectName,
(err, project) => {
// have a crack at setting the root doc after a while, on creation we won't have it yet, but should have
// been sent it it within 30 seconds
setTimeout(
() =>
ProjectRootDocManager.setRootDocAutomatically(project._id),
this._rootDocTimeoutLength
)
return cb(err, project)
}
)
} else {
if (isArchivedOrTrashed) {
return cb(new Errors.ProjectIsArchivedOrTrashedError())
}
return cb(err, project)
}
}
)
function newUpdate(userId, projectName, path, updateRequest, source, callback) {
getOrCreateProject(userId, projectName, (err, project) => {
if (err) {
return callback(err)
}
return getOrCreateProject(function(err, project) {
if (err != null) {
return callback(err)
}
return CooldownManager.isProjectOnCooldown(project._id, function(
err,
projectIsOnCooldown
) {
if (err != null) {
if (project == null) {
return callback()
}
CooldownManager.isProjectOnCooldown(
project._id,
(err, projectIsOnCooldown) => {
if (err) {
return callback(err)
}
if (projectIsOnCooldown) {
@@ -78,12 +32,12 @@ module.exports = {
new Errors.TooManyRequestsError('project on cooldown')
)
}
return FileTypeManager.shouldIgnore(path, function(err, shouldIgnore) {
FileTypeManager.shouldIgnore(path, (err, shouldIgnore) => {
if (shouldIgnore) {
return callback()
}
return updateMerger.mergeUpdate(
user_id,
UpdateMerger.mergeUpdate(
userId,
project._id,
path,
updateRequest,
@@ -91,57 +45,115 @@ module.exports = {
callback
)
})
})
})
},
}
)
})
}
deleteUpdate(user_id, projectName, path, source, callback) {
logger.log({ user_id, filePath: path }, 'handling delete update from tpds')
return projectLocator.findUsersProjectByName(user_id, projectName, function(
err,
result
) {
function deleteUpdate(userId, projectName, path, source, callback) {
logger.debug({ userId, filePath: path }, 'handling delete update from tpds')
ProjectGetter.findUsersProjectsByName(
userId,
projectName,
(err, projects) => {
if (err) {
return callback(err)
}
if (!result) {
return callback(new Error('no data from project locator'))
}
const { project, isArchivedOrTrashed } = result
if (project == null) {
logger.log(
{ user_id, filePath: path, projectName },
const activeProjects = projects.filter(
project => !ProjectHelper.isArchivedOrTrashed(project, userId)
)
if (activeProjects.length === 0) {
logger.debug(
{ userId, filePath: path, projectName },
'project not found from tpds update, ignoring folder or project'
)
return callback()
}
if (isArchivedOrTrashed) {
logger.log(
{ user_id, filePath: path, projectName },
'project is archived or trashed, ignoring folder or project'
)
return callback()
if (projects.length > 1) {
// There is more than one project with that name, and one of them is
// active (previous condition)
return handleDuplicateProjects(userId, projectName, callback)
}
const project = activeProjects[0]
if (path === '/') {
logger.log(
{ user_id, filePath: path, projectName, project_id: project._id },
logger.debug(
{ userId, filePath: path, projectName, project_id: project._id },
'project found for delete update, path is root so marking project as deleted'
)
return projectDeleter.markAsDeletedByExternalSource(
project._id,
callback
)
ProjectDeleter.markAsDeletedByExternalSource(project._id, callback)
} else {
return updateMerger.deleteUpdate(
user_id,
project._id,
path,
source,
err => callback(err)
UpdateMerger.deleteUpdate(userId, project._id, path, source, err => {
callback(err)
})
}
}
)
}
function getOrCreateProject(userId, projectName, callback) {
ProjectGetter.findUsersProjectsByName(
userId,
projectName,
(err, projects) => {
if (err) {
return callback(err)
}
if (projects.length === 0) {
// No project with that name -- active, archived or trashed -- has been
// found. Create one.
return ProjectCreationHandler.createBlankProject(
userId,
projectName,
(err, project) => {
// have a crack at setting the root doc after a while, on creation
// we won't have it yet, but should have been sent it it within 30
// seconds
setTimeout(() => {
ProjectRootDocManager.setRootDocAutomatically(project._id)
}, ROOT_DOC_TIMEOUT_LENGTH)
callback(err, project)
}
)
}
})
},
const activeProjects = projects.filter(
project => !ProjectHelper.isArchivedOrTrashed(project, userId)
)
if (activeProjects.length === 0) {
// All projects with that name are archived or trashed. Ignore.
return callback(null, null)
}
_rootDocTimeoutLength: 30 * 1000
if (projects.length > 1) {
// There is more than one project with that name, and one of them is
// active (previous condition)
return handleDuplicateProjects(userId, projectName, err => {
if (err) {
return callback(err)
}
callback(null, null)
})
}
callback(err, activeProjects[0])
}
)
}
function handleDuplicateProjects(userId, projectName, callback) {
Modules.hooks.fire('removeDropbox', userId, err => {
if (err) {
return callback(err)
}
NotificationsBuilder.dropboxDuplicateProjectNames(userId).create(
projectName,
callback
)
})
}
module.exports = {
newUpdate,
deleteUpdate
}
+129 -172
View File
@@ -1,194 +1,151 @@
/* eslint-disable
camelcase,
max-len,
no-path-concat,
no-unused-vars,
one-var,
*/
// 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
* DS103: Rewrite code to no longer use __guard__
* DS201: Simplify complex destructure assignments
* 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
*/
let Modules
const fs = require('fs')
const Path = require('path')
const pug = require('pug')
const async = require('async')
const { promisify } = require('util')
const Settings = require('settings-sharelatex')
const MODULE_BASE_PATH = Path.resolve(__dirname + '/../../../modules')
module.exports = Modules = {
modules: [],
loadModules() {
for (let moduleName of Array.from(fs.readdirSync(MODULE_BASE_PATH))) {
if (fs.existsSync(Path.join(MODULE_BASE_PATH, moduleName, 'index.js'))) {
const loadedModule = require(Path.join(
MODULE_BASE_PATH,
moduleName,
'index'
))
loadedModule.name = moduleName
this.modules.push(loadedModule)
}
}
return Modules.attachHooks()
},
const _modules = []
const _hooks = {}
let _viewIncludes = {}
applyRouter(webRouter, privateApiRouter, publicApiRouter) {
return Array.from(this.modules).map(module =>
__guardMethod__(module.router, 'apply', o =>
o.apply(webRouter, privateApiRouter, publicApiRouter)
function loadModules() {
for (let moduleName of fs.readdirSync(MODULE_BASE_PATH)) {
if (fs.existsSync(Path.join(MODULE_BASE_PATH, moduleName, 'index.js'))) {
const loadedModule = require(Path.join(
MODULE_BASE_PATH,
moduleName,
'index'
))
loadedModule.name = moduleName
_modules.push(loadedModule)
}
}
attachHooks()
}
function applyRouter(webRouter, privateApiRouter, publicApiRouter) {
for (const module of _modules) {
if (module.router && module.router.apply) {
module.router.apply(webRouter, privateApiRouter, publicApiRouter)
}
}
}
function applyNonCsrfRouter(webRouter, privateApiRouter, publicApiRouter) {
for (let module of _modules) {
if (module.nonCsrfRouter != null) {
module.nonCsrfRouter.apply(webRouter, privateApiRouter, publicApiRouter)
}
if (module.router && module.router.applyNonCsrfRouter) {
module.router.applyNonCsrfRouter(
webRouter,
privateApiRouter,
publicApiRouter
)
)
},
applyNonCsrfRouter(webRouter, privateApiRouter, publicApiRouter) {
return (() => {
const result = []
for (let module of Array.from(this.modules)) {
if (module.nonCsrfRouter != null) {
module.nonCsrfRouter.apply(
webRouter,
privateApiRouter,
publicApiRouter
)
}
result.push(
__guardMethod__(module.router, 'applyNonCsrfRouter', o =>
o.applyNonCsrfRouter(webRouter, privateApiRouter, publicApiRouter)
)
)
}
return result
})()
},
viewIncludes: {},
loadViewIncludes(app) {
this.viewIncludes = {}
return Array.from(this.modules).map(module =>
(() => {
const result = []
const object = module.viewIncludes || {}
for (let view in object) {
const partial = object[view]
if (!this.viewIncludes[view]) {
this.viewIncludes[view] = []
}
const filePath = Path.join(
MODULE_BASE_PATH,
module.name,
'app/views',
partial + '.pug'
)
result.push(
this.viewIncludes[view].push(
pug.compileFile(filePath, {
doctype: 'html',
compileDebug: Settings.debugPugTemplates
})
)
)
}
return result
})()
)
},
moduleIncludes(view, locals) {
const compiledPartials = Modules.viewIncludes[view] || []
let html = ''
for (let compiledPartial of Array.from(compiledPartials)) {
const d = new Date()
html += compiledPartial(locals)
}
return html
},
}
}
moduleIncludesAvailable(view) {
return (Modules.viewIncludes[view] || []).length > 0
},
function loadViewIncludes(app) {
_viewIncludes = {}
for (const module of _modules) {
const object = module.viewIncludes || {}
for (let view in object) {
const partial = object[view]
if (!_viewIncludes[view]) {
_viewIncludes[view] = []
}
const filePath = Path.join(
MODULE_BASE_PATH,
module.name,
'app/views',
partial + '.pug'
)
_viewIncludes[view].push(
pug.compileFile(filePath, {
doctype: 'html',
compileDebug: Settings.debugPugTemplates
})
)
}
}
}
linkedFileAgentsIncludes() {
const agents = {}
for (let module of Array.from(this.modules)) {
for (let name in module.linkedFileAgents) {
const agentFunction = module.linkedFileAgents[name]
agents[name] = agentFunction()
function moduleIncludes(view, locals) {
const compiledPartials = _viewIncludes[view] || []
let html = ''
for (let compiledPartial of compiledPartials) {
html += compiledPartial(locals)
}
return html
}
function moduleIncludesAvailable(view) {
return (_viewIncludes[view] || []).length > 0
}
function linkedFileAgentsIncludes() {
const agents = {}
for (let module of _modules) {
for (let name in module.linkedFileAgents) {
const agentFunction = module.linkedFileAgents[name]
agents[name] = agentFunction()
}
}
return agents
}
function attachHooks() {
for (var module of _modules) {
if (module.hooks != null) {
for (let hook in module.hooks) {
const method = module.hooks[hook]
attachHook(hook, method)
}
}
return agents
},
}
}
attachHooks() {
return (() => {
const result = []
for (var module of Array.from(this.modules)) {
if (module.hooks != null) {
result.push(
(() => {
const result1 = []
for (let hook in module.hooks) {
const method = module.hooks[hook]
result1.push(Modules.hooks.attach(hook, method))
}
return result1
})()
)
} else {
result.push(undefined)
}
}
return result
})()
},
function attachHook(name, method) {
if (_hooks[name] == null) {
_hooks[name] = []
}
_hooks[name].push(method)
}
function fireHook(name, ...rest) {
const adjustedLength = Math.max(rest.length, 1)
const args = rest.slice(0, adjustedLength - 1)
const callback = rest[adjustedLength - 1]
const methods = _hooks[name] || []
const callMethods = methods.map(method => cb => method(...args, cb))
async.series(callMethods, function(error, results) {
if (error) {
return callback(error)
}
callback(null, results)
})
}
module.exports = {
applyNonCsrfRouter,
applyRouter,
linkedFileAgentsIncludes,
loadViewIncludes,
moduleIncludes,
moduleIncludesAvailable,
hooks: {
_hooks: {},
attach(name, method) {
if (this._hooks[name] == null) {
this._hooks[name] = []
}
return this._hooks[name].push(method)
},
fire(name, ...rest) {
const adjustedLength = Math.max(rest.length, 1),
args = rest.slice(0, adjustedLength - 1),
callback = rest[adjustedLength - 1]
const methods = this._hooks[name] || []
const call_methods = methods.map(method => cb =>
method(...Array.from(args), cb)
)
return async.series(call_methods, function(error, results) {
if (error != null) {
return callback(error)
}
return callback(null, results)
})
attach: attachHook,
fire: fireHook
},
promises: {
hooks: {
fire: promisify(fireHook)
}
}
}
Modules.loadModules()
function __guardMethod__(obj, methodName, transform) {
if (
typeof obj !== 'undefined' &&
obj !== null &&
typeof obj[methodName] === 'function'
) {
return transform(obj, methodName)
} else {
return undefined
}
}
loadModules()
@@ -106,6 +106,16 @@ include ../../_mixins/saml
span(aria-hidden="true") &times;
span.sr-only #{translate("close")}
.alert.alert-warning(
ng-switch-when="notification_dropbox_duplicate_project_names"
)
.notification-body
| !{translate("dropbox_duplicate_project_names", {projectName: '{{notification.messageOpts.projectName}}'}, ['strong'])}
.notification-close
button.btn-sm(ng-click="dismiss(notification)").close.pull-right
span(aria-hidden="true") &times;
span.sr-only #{translate("close")}
.alert.alert-info(
ng-switch-default
)
+2 -1
View File
@@ -1343,5 +1343,6 @@
"add_affiliation": "Add Affiliation",
"did_you_know_institution_providing_professional": "Did you know that __institutionName__ is providing <0>free __appName__ Professional features</0> to everyone at __institutionName__?",
"add_email_to_claim_features": "Add an institutional email address to claim your features.",
"please_change_primary_to_remove": "Please change your primary email in order to remove"
"please_change_primary_to_remove": "Please change your primary email in order to remove",
"dropbox_duplicate_project_names": "We detected an update from Dropbox to <0>__projectName__</0>, but you have multiple projects with that name. We are unable to process this, so have unlinked your Dropbox account. Please ensure your project names are unique across your active, archived and trashed projects, and then re-link your Dropbox account."
}
@@ -171,7 +171,7 @@ describe('TpdsUpdateTests', function() {
if (error != null) {
throw error
}
expect(response.statusCode).to.equal(409)
expect(response.statusCode).to.equal(200)
return done()
}
)
@@ -1,91 +1,88 @@
/* eslint-disable
camelcase,
max-len,
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/src/Features/Project/ProjectGetter.js'
const SandboxedModule = require('sandboxed-module')
const { ObjectId } = require('mongodb')
const { normalizeQuery } = require('../../../../app/src/Features/Helpers/Mongo')
const { assert } = require('chai')
describe('ProjectGetter', function() {
beforeEach(function() {
this.callback = sinon.stub()
this.project = { _id: new ObjectId() }
this.projectIdStr = this.project._id.toString()
this.deletedProject = { deleterData: { wombat: 'potato' } }
this.userId = new ObjectId()
this.DeletedProject = {
find: sinon.stub().yields(null, [this.deletedProject])
}
return (this.ProjectGetter = SandboxedModule.require(modulePath, {
globals: {
console: console
this.Project = {
find: sinon.stub(),
findOne: sinon.stub().yields(null, this.project)
}
this.CollaboratorsGetter = {
getProjectsUserIsMemberOf: sinon.stub().yields(null, {
readAndWrite: [],
readOnly: [],
tokenReadAndWrite: [],
tokenReadOnly: []
})
}
this.LockManager = {
runWithLock: sinon
.stub()
.callsFake((namespace, id, runner, callback) => runner(callback))
}
this.db = {
projects: {
findOne: sinon.stub().yields(null, this.project)
},
users: {}
}
this.ProjectEntityMongoUpdateHandler = {
lockKey: sinon.stub().returnsArg(0)
}
this.ProjectGetter = SandboxedModule.require(modulePath, {
globals: { console },
requires: {
'../../infrastructure/mongodb': {
db: (this.db = {
projects: {},
users: {}
}),
ObjectId
},
'../../infrastructure/mongodb': { db: this.db, ObjectId },
'@overleaf/metrics': {
timeAsyncMethod: sinon.stub()
},
'../../models/Project': {
Project: (this.Project = {})
Project: this.Project
},
'../../models/DeletedProject': {
DeletedProject: this.DeletedProject
},
'../Collaborators/CollaboratorsGetter': (this.CollaboratorsGetter = {}),
'../../infrastructure/LockManager': (this.LockManager = {
runWithLock: sinon.spy((namespace, id, runner, callback) =>
runner(callback)
)
}),
'./ProjectEntityMongoUpdateHandler': {
lockKey(project_id) {
return project_id
}
},
'../Helpers/Mongo': { normalizeQuery },
'../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
'../../infrastructure/LockManager': this.LockManager,
'./ProjectEntityMongoUpdateHandler': this
.ProjectEntityMongoUpdateHandler,
'logger-sharelatex': {
err() {},
log() {}
}
}
}))
})
})
describe('getProjectWithoutDocLines', function() {
beforeEach(function() {
this.project = { _id: (this.project_id = '56d46b0a1d3422b87c5ebcb1') }
return (this.ProjectGetter.getProject = sinon.stub().yields())
this.ProjectGetter.getProject = sinon.stub().yields()
})
describe('passing an id', function() {
beforeEach(function() {
return this.ProjectGetter.getProjectWithoutDocLines(
this.project_id,
this.ProjectGetter.getProjectWithoutDocLines(
this.project._id,
this.callback
)
})
it('should call find with the project id', function() {
return this.ProjectGetter.getProject
.calledWith(this.project_id)
this.ProjectGetter.getProject
.calledWith(this.project._id)
.should.equal(true)
})
@@ -101,34 +98,33 @@ describe('ProjectGetter', function() {
'rootFolder.folders.folders.folders.folders.folders.folders.folders.docs.lines': 0
}
return this.ProjectGetter.getProject
.calledWith(this.project_id, excludes)
this.ProjectGetter.getProject
.calledWith(this.project._id, excludes)
.should.equal(true)
})
it('should call the callback', function() {
return this.callback.called.should.equal(true)
this.callback.called.should.equal(true)
})
})
})
describe('getProjectWithOnlyFolders', function() {
beforeEach(function() {
this.project = { _id: (this.project_id = '56d46b0a1d3422b87c5ebcb1') }
return (this.ProjectGetter.getProject = sinon.stub().yields())
this.ProjectGetter.getProject = sinon.stub().yields()
})
describe('passing an id', function() {
beforeEach(function() {
return this.ProjectGetter.getProjectWithOnlyFolders(
this.project_id,
this.ProjectGetter.getProjectWithOnlyFolders(
this.project._id,
this.callback
)
})
it('should call find with the project id', function() {
return this.ProjectGetter.getProject
.calledWith(this.project_id)
this.ProjectGetter.getProject
.calledWith(this.project._id)
.should.equal(true)
})
@@ -151,62 +147,53 @@ describe('ProjectGetter', function() {
'rootFolder.folders.folders.folders.folders.folders.folders.folders.docs': 0,
'rootFolder.folders.folders.folders.folders.folders.folders.folders.fileRefs': 0
}
return this.ProjectGetter.getProject
.calledWith(this.project_id, excludes)
this.ProjectGetter.getProject
.calledWith(this.project._id, excludes)
.should.equal(true)
})
it('should call the callback with the project', function() {
return this.callback.called.should.equal(true)
this.callback.called.should.equal(true)
})
})
})
describe('getProject', function() {
beforeEach(function() {
this.project = { _id: (this.project_id = '56d46b0a1d3422b87c5ebcb1') }
return (this.db.projects.findOne = sinon
.stub()
.callsArgWith(2, null, this.project))
})
describe('without projection', function() {
describe('with project id', function() {
beforeEach(function() {
return this.ProjectGetter.getProject(this.project_id, this.callback)
this.ProjectGetter.getProject(this.projectIdStr, this.callback)
})
it('should call findOne with the project id', function() {
expect(this.db.projects.findOne.callCount).to.equal(1)
return expect(
this.db.projects.findOne.lastCall.args[0]
).to.deep.equal({
_id: ObjectId(this.project_id)
})
expect(
this.db.projects.findOne.lastCall.args[0]._id.toString()
).to.equal(this.projectIdStr)
})
})
describe('without project id', function() {
beforeEach(function() {
return this.ProjectGetter.getProject(null, this.callback)
this.ProjectGetter.getProject(null, this.callback)
})
it('should callback with error', function() {
expect(this.db.projects.findOne.callCount).to.equal(0)
return expect(this.callback.lastCall.args[0]).to.be.instanceOf(Error)
expect(this.callback.lastCall.args[0]).to.be.instanceOf(Error)
})
})
})
describe('with projection', function() {
beforeEach(function() {
return (this.projection = { _id: 1 })
this.projection = { _id: 1 }
})
describe('with project id', function() {
beforeEach(function() {
return this.ProjectGetter.getProject(
this.project_id,
this.ProjectGetter.getProject(
this.projectIdStr,
this.projection,
this.callback
)
@@ -214,76 +201,67 @@ describe('ProjectGetter', function() {
it('should call findOne with the project id', function() {
expect(this.db.projects.findOne.callCount).to.equal(1)
expect(this.db.projects.findOne.lastCall.args[0]).to.deep.equal({
_id: ObjectId(this.project_id)
expect(
this.db.projects.findOne.lastCall.args[0]._id.toString()
).to.equal(this.projectIdStr)
expect(this.db.projects.findOne.lastCall.args[1]).to.deep.equal({
projection: this.projection
})
return expect(
this.db.projects.findOne.lastCall.args[1]
).to.deep.equal({ projection: this.projection })
})
})
describe('without project id', function() {
beforeEach(function() {
return this.ProjectGetter.getProject(null, this.callback)
this.ProjectGetter.getProject(null, this.callback)
})
it('should callback with error', function() {
expect(this.db.projects.findOne.callCount).to.equal(0)
return expect(this.callback.lastCall.args[0]).to.be.instanceOf(Error)
expect(this.callback.lastCall.args[0]).to.be.instanceOf(Error)
})
})
})
})
describe('getProjectWithoutLock', function() {
beforeEach(function() {
this.project = { _id: (this.project_id = '56d46b0a1d3422b87c5ebcb1') }
return (this.db.projects.findOne = sinon
.stub()
.callsArgWith(2, null, this.project))
})
describe('without projection', function() {
describe('with project id', function() {
beforeEach(function() {
return this.ProjectGetter.getProjectWithoutLock(
this.project_id,
this.ProjectGetter.getProjectWithoutLock(
this.projectIdStr,
this.callback
)
})
it('should call findOne with the project id', function() {
expect(this.db.projects.findOne.callCount).to.equal(1)
return expect(
this.db.projects.findOne.lastCall.args[0]
).to.deep.equal({
_id: ObjectId(this.project_id)
})
expect(
this.db.projects.findOne.lastCall.args[0]._id.toString()
).to.equal(this.projectIdStr)
})
})
describe('without project id', function() {
beforeEach(function() {
return this.ProjectGetter.getProjectWithoutLock(null, this.callback)
this.ProjectGetter.getProjectWithoutLock(null, this.callback)
})
it('should callback with error', function() {
expect(this.db.projects.findOne.callCount).to.equal(0)
return expect(this.callback.lastCall.args[0]).to.be.instanceOf(Error)
expect(this.callback.lastCall.args[0]).to.be.instanceOf(Error)
})
})
})
describe('with projection', function() {
beforeEach(function() {
return (this.projection = { _id: 1 })
this.projection = { _id: 1 }
})
describe('with project id', function() {
beforeEach(function() {
return this.ProjectGetter.getProjectWithoutLock(
this.project_id,
this.ProjectGetter.getProjectWithoutLock(
this.project._id,
this.projection,
this.callback
)
@@ -291,23 +269,23 @@ describe('ProjectGetter', function() {
it('should call findOne with the project id', function() {
expect(this.db.projects.findOne.callCount).to.equal(1)
expect(this.db.projects.findOne.lastCall.args[0]).to.deep.equal({
_id: ObjectId(this.project_id)
expect(
this.db.projects.findOne.lastCall.args[0]._id.toString()
).to.equal(this.projectIdStr)
expect(this.db.projects.findOne.lastCall.args[1]).to.deep.equal({
projection: this.projection
})
return expect(
this.db.projects.findOne.lastCall.args[1]
).to.deep.equal({ projection: this.projection })
})
})
describe('without project id', function() {
beforeEach(function() {
return this.ProjectGetter.getProjectWithoutLock(null, this.callback)
this.ProjectGetter.getProjectWithoutLock(null, this.callback)
})
it('should callback with error', function() {
expect(this.db.projects.findOne.callCount).to.equal(0)
return expect(this.callback.lastCall.args[0]).to.be.instanceOf(Error)
expect(this.callback.lastCall.args[0]).to.be.instanceOf(Error)
})
})
})
@@ -316,28 +294,24 @@ describe('ProjectGetter', function() {
describe('findAllUsersProjects', function() {
beforeEach(function() {
this.fields = { mock: 'fields' }
this.Project.find = sinon.stub()
this.Project.find
.withArgs({ owner_ref: this.user_id }, this.fields)
.withArgs({ owner_ref: this.userId }, this.fields)
.yields(null, ['mock-owned-projects'])
this.CollaboratorsGetter.getProjectsUserIsMemberOf = sinon.stub()
this.CollaboratorsGetter.getProjectsUserIsMemberOf
.withArgs(this.user_id, this.fields)
.yields(null, {
readAndWrite: ['mock-rw-projects'],
readOnly: ['mock-ro-projects'],
tokenReadAndWrite: ['mock-token-rw-projects'],
tokenReadOnly: ['mock-token-ro-projects']
})
return this.ProjectGetter.findAllUsersProjects(
this.user_id,
this.CollaboratorsGetter.getProjectsUserIsMemberOf.yields(null, {
readAndWrite: ['mock-rw-projects'],
readOnly: ['mock-ro-projects'],
tokenReadAndWrite: ['mock-token-rw-projects'],
tokenReadOnly: ['mock-token-ro-projects']
})
this.ProjectGetter.findAllUsersProjects(
this.userId,
this.fields,
this.callback
)
})
it('should call the callback with all the projects', function() {
return this.callback
this.callback
.calledWith(null, {
owned: ['mock-owned-projects'],
readAndWrite: ['mock-rw-projects'],
@@ -352,53 +326,104 @@ describe('ProjectGetter', function() {
describe('getProjectIdByReadAndWriteToken', function() {
describe('when project find returns project', function() {
this.beforeEach(function() {
this.Project.findOne = sinon.stub().yields(null, { _id: 'project-id' })
return this.ProjectGetter.getProjectIdByReadAndWriteToken(
this.ProjectGetter.getProjectIdByReadAndWriteToken(
'token',
this.callback
)
})
it('should find project with token', function() {
return this.Project.findOne
this.Project.findOne
.calledWithMatch({ 'tokens.readAndWrite': 'token' })
.should.equal(true)
})
it('should callback with project id', function() {
return this.callback.calledWith(null, 'project-id').should.equal(true)
this.callback.calledWith(null, this.project._id).should.equal(true)
})
})
describe('when project not found', function() {
this.beforeEach(function() {
this.Project.findOne = sinon.stub().yields()
return this.ProjectGetter.getProjectIdByReadAndWriteToken(
this.Project.findOne.yields(null, null)
this.ProjectGetter.getProjectIdByReadAndWriteToken(
'token',
this.callback
)
})
it('should callback empty', function() {
return expect(this.callback.firstCall.args.length).to.equal(0)
expect(this.callback.firstCall.args.length).to.equal(0)
})
})
describe('when project find returns error', function() {
this.beforeEach(function() {
this.Project.findOne = sinon.stub().yields('error')
return this.ProjectGetter.getProjectIdByReadAndWriteToken(
this.Project.findOne.yields('error')
this.ProjectGetter.getProjectIdByReadAndWriteToken(
'token',
this.callback
)
})
it('should callback with error', function() {
return this.callback.calledWith('error').should.equal(true)
this.callback.calledWith('error').should.equal(true)
})
})
})
describe('findUsersProjectsByName', function() {
it('should perform a case-insensitive search', function(done) {
this.Project.find
.withArgs({ owner_ref: this.userId })
.yields(null, [
{ name: 'find me!' },
{ name: 'not me!' },
{ name: 'FIND ME!' },
{ name: 'Find Me!' }
])
this.ProjectGetter.findUsersProjectsByName(
this.userId,
'find me!',
(err, projects) => {
if (err != null) {
return done(err)
}
projects
.map(project => project.name)
.should.have.members(['find me!', 'FIND ME!', 'Find Me!'])
done()
}
)
})
it('should search collaborations as well', function(done) {
this.Project.find
.withArgs({ owner_ref: this.userId })
.yields(null, [{ name: 'find me!' }])
this.CollaboratorsGetter.getProjectsUserIsMemberOf.yields(null, {
readAndWrite: [{ name: 'FIND ME!' }],
readOnly: [{ name: 'Find Me!' }],
tokenReadAndWrite: [{ name: 'find ME!' }],
tokenReadOnly: [{ name: 'FIND me!' }]
})
this.ProjectGetter.findUsersProjectsByName(
this.userId,
'find me!',
(err, projects) => {
if (err != null) {
return done(err)
}
expect(projects.map(project => project.name)).to.have.members([
'find me!',
'FIND ME!'
])
done()
}
)
})
})
describe('getUsersDeletedProjects', function() {
it('should look up the deleted projects by deletedProjectOwnerId', function(done) {
this.ProjectGetter.getUsersDeletedProjects('giraffe', err => {
@@ -565,163 +565,4 @@ describe('ProjectLocator', function() {
})
})
})
describe('findUsersProjectByName finding a project by user_id and project name', function() {
it('should return the project from an array case insenstive', function(done) {
const userId = '123jojoidns'
const stubbedProject = { name: 'findThis' }
const projects = {
owned: [
{ name: 'notThis' },
{ name: 'wellll' },
stubbedProject,
{ name: 'Noooo' }
],
readAndWrite: []
}
this.ProjectGetter.findAllUsersProjects = sinon
.stub()
.callsArgWith(2, null, projects)
this.locator.findUsersProjectByName(
userId,
stubbedProject.name.toLowerCase(),
(err, result) => {
if (err != null) {
return done(err)
}
expect(result).to.exist
const { project } = result
project.should.equal(stubbedProject)
done()
}
)
})
it('should return the project which is not archived first', function(done) {
const userId = '123jojoidns'
const stubbedProject = { name: 'findThis', _id: 12331321 }
const projects = {
owned: [
{ name: 'notThis' },
{ name: 'wellll' },
{ name: 'findThis', archived: true, trashed: true },
stubbedProject,
{ name: 'findThis', archived: true, trashed: false },
{ name: 'Noooo', trashed: true }
],
readAndWrite: []
}
this.ProjectHelper.isArchivedOrTrashed
.withArgs(projects.owned[0], userId)
.returns(false)
this.ProjectHelper.isArchivedOrTrashed
.withArgs(projects.owned[1], userId)
.returns(false)
this.ProjectHelper.isArchivedOrTrashed
.withArgs(projects.owned[2], userId)
.returns(true)
this.ProjectHelper.isArchivedOrTrashed
.withArgs(projects.owned[3], userId)
.returns(false)
this.ProjectHelper.isArchivedOrTrashed
.withArgs(projects.owned[4], userId)
.returns(true)
this.ProjectHelper.isArchivedOrTrashed
.withArgs(projects.owned[5], userId)
.returns(true)
this.ProjectGetter.findAllUsersProjects = sinon
.stub()
.callsArgWith(2, null, projects)
this.locator.findUsersProjectByName(
userId,
stubbedProject.name.toLowerCase(),
(err, result) => {
if (err != null) {
return done(err)
}
expect(result).to.exist
const { project, isArchivedOrTrashed } = result
project._id.should.equal(stubbedProject._id)
expect(isArchivedOrTrashed).to.equal(false)
done()
}
)
})
it('should return archived project, and a flag indicating it is archived', function(done) {
const userId = '123jojoidns'
const stubbedProject = { name: 'findThis', _id: 12331321 }
const projects = {
owned: [
{ name: 'notThis' },
{ name: 'wellll' },
{ name: 'findThis', archived: true, trashed: true, _id: 1234 },
{ name: 'findThis', archived: true, trashed: false },
{ name: 'Noooo', trashed: true }
],
readAndWrite: []
}
this.ProjectHelper.isArchivedOrTrashed
.withArgs(projects.owned[0], userId)
.returns(false)
this.ProjectHelper.isArchivedOrTrashed
.withArgs(projects.owned[1], userId)
.returns(false)
this.ProjectHelper.isArchivedOrTrashed
.withArgs(projects.owned[2], userId)
.returns(true)
this.ProjectHelper.isArchivedOrTrashed
.withArgs(projects.owned[3], userId)
.returns(true)
this.ProjectHelper.isArchivedOrTrashed
.withArgs(projects.owned[4], userId)
.returns(true)
this.ProjectGetter.findAllUsersProjects = sinon
.stub()
.callsArgWith(2, null, projects)
this.locator.findUsersProjectByName(
userId,
stubbedProject.name.toLowerCase(),
(err, result) => {
if (err != null) {
return done(err)
}
expect(result).to.exist
const { project, isArchivedOrTrashed } = result
project._id.should.equal(1234)
expect(isArchivedOrTrashed).to.equal(true)
done()
}
)
})
it('should search collab projects as well', function(done) {
const userId = '123jojoidns'
const stubbedProject = { name: 'findThis' }
const projects = {
owned: [{ name: 'notThis' }, { name: 'wellll' }, { name: 'Noooo' }],
readAndWrite: [stubbedProject]
}
this.ProjectGetter.findAllUsersProjects = sinon
.stub()
.callsArgWith(2, null, projects)
this.locator.findUsersProjectByName(
userId,
stubbedProject.name.toLowerCase(),
(err, result) => {
if (err != null) {
return done(err)
}
expect(result).to.exist
const { project } = result
project.should.equal(stubbedProject)
done()
}
)
})
})
})
@@ -1,14 +1,3 @@
/* eslint-disable
max-len,
no-return-assign,
*/
// 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 SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
require('chai').should()
@@ -110,28 +99,6 @@ describe('TpdsController', function() {
res.sendStatus.calledWith(500).should.equal(true)
})
it('should return a 409 error when the project is archived', function() {
const path = '/projectName/here.txt'
const req = {
pause() {},
params: { 0: path, user_id: this.user_id },
session: {
destroy() {}
},
headers: {
'x-sl-update-source': (this.source = 'dropbox')
}
}
this.TpdsUpdateHandler.newUpdate = sinon
.stub()
.callsArgWith(5, new Errors.ProjectIsArchivedOrTrashedError())
const res = {
sendStatus: sinon.stub()
}
this.TpdsController.mergeUpdate(req, res)
res.sendStatus.calledWith(409).should.equal(true)
})
it('should return a 400 error when the project is too big', function() {
const path = '/projectName/here.txt'
const req = {
@@ -181,7 +148,7 @@ describe('TpdsController', function() {
})
describe('getting a delete update', function() {
it('should process the delete with the update reciver', function(done) {
it('should process the delete with the update receiver', function(done) {
const path = '/projectName/here.txt'
const req = {
params: { 0: path, user_id: this.user_id },
@@ -210,7 +177,7 @@ describe('TpdsController', function() {
const path = 'noSlashHere'
const req = { params: { 0: path, user_id: this.user_id } }
const result = this.TpdsController.parseParams(req)
result.user_id.should.equal(this.user_id)
result.userId.should.equal(this.user_id)
result.filePath.should.equal('/')
result.projectName.should.equal(path)
})
@@ -219,7 +186,7 @@ describe('TpdsController', function() {
const path = '/project/file.tex'
const req = { params: { 0: path, user_id: this.user_id } }
const result = this.TpdsController.parseParams(req)
result.user_id.should.equal(this.user_id)
result.userId.should.equal(this.user_id)
result.filePath.should.equal('/file.tex')
result.projectName.should.equal('project')
})
@@ -250,7 +217,7 @@ describe('TpdsController', function() {
}
this.res = { sendStatus: sinon.stub() }
this.TpdsController.updateProjectContents(this.req, this.res)
this.TpdsController.updateProjectContents(this.req, this.res, this.next)
})
it('should merge the update', function() {
@@ -287,7 +254,7 @@ describe('TpdsController', function() {
}
this.res = { sendStatus: sinon.stub() }
this.TpdsController.deleteProjectContents(this.req, this.res)
this.TpdsController.deleteProjectContents(this.req, this.res, this.next)
})
it('should delete the file', function() {
@@ -1,322 +1,436 @@
/* eslint-disable
camelcase,
max-len,
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 SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
require('chai').should()
const { expect } = require('chai')
const modulePath = require('path').join(
__dirname,
const { ObjectId } = require('mongodb')
const Errors = require('../../../../app/src/Features/Errors/Errors')
const MODULE_PATH =
'../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateHandler.js'
)
describe('TpdsUpdateHandler', function() {
beforeEach(function() {
this.requestQueuer = {}
this.updateMerger = {
deleteUpdate(user_id, project_id, path, source, cb) {
return cb()
this.clock = sinon.useFakeTimers()
})
afterEach(function() {
this.clock.restore()
})
beforeEach(function() {
this.projectName = 'My recipes'
this.projects = {
active1: { _id: new ObjectId(), name: this.projectName },
active2: { _id: new ObjectId(), name: this.projectName },
archived1: {
_id: new ObjectId(),
name: this.projectName,
archived: [this.userId]
},
mergeUpdate(user_id, project_id, path, update, source, cb) {
return cb()
archived2: {
_id: new ObjectId(),
name: this.projectName,
archived: [this.userId]
}
}
this.editorController = {}
this.project_id = 'dsjajilknaksdn'
this.project = { _id: this.project_id, name: 'projectNameHere' }
this.projectLocator = {
findUsersProjectByName: sinon
.stub()
.callsArgWith(2, null, { project: this.project })
}
this.projectCreationHandler = {
createBlankProject: sinon.stub().callsArgWith(2, null, this.project)
}
this.projectDeleter = {
markAsDeletedByExternalSource: sinon.stub().callsArgWith(1)
}
this.rootDocManager = { setRootDocAutomatically: sinon.stub() }
this.FileTypeManager = {
shouldIgnore: sinon.stub().callsArgWith(1, null, false)
}
this.userId = new ObjectId()
this.source = 'dropbox'
this.path = `/some/file`
this.update = {}
this.CooldownManager = {
isProjectOnCooldown: sinon.stub().callsArgWith(1, null, false)
isProjectOnCooldown: sinon.stub().yields(null, false)
}
this.handler = SandboxedModule.require(modulePath, {
globals: {
console: console
},
this.FileTypeManager = {
shouldIgnore: sinon.stub().yields(null, false)
}
this.Modules = {
hooks: { fire: sinon.stub().yields() }
}
this.notification = {
create: sinon.stub().yields()
}
this.NotificationsBuilder = {
dropboxDuplicateProjectNames: sinon.stub().returns(this.notification)
}
this.ProjectCreationHandler = {
createBlankProject: sinon.stub().yields(null, this.projects.active1)
}
this.ProjectDeleter = {
markAsDeletedByExternalSource: sinon.stub().yields()
}
this.ProjectGetter = {
findUsersProjectsByName: sinon.stub()
}
this.ProjectHelper = {
isArchivedOrTrashed: sinon.stub().returns(false)
}
this.ProjectHelper.isArchivedOrTrashed
.withArgs(this.projects.archived1, this.userId)
.returns(true)
this.ProjectHelper.isArchivedOrTrashed
.withArgs(this.projects.archived2, this.userId)
.returns(true)
this.RootDocManager = { setRootDocAutomatically: sinon.stub() }
this.UpdateMerger = {
deleteUpdate: sinon.stub().yields(),
mergeUpdate: sinon.stub().yields()
}
this.TpdsUpdateHandler = SandboxedModule.require(MODULE_PATH, {
globals: { console },
requires: {
'./UpdateMerger': this.updateMerger,
'./Editor/EditorController': this.editorController,
'../Project/ProjectLocator': this.projectLocator,
'../Project/ProjectCreationHandler': this.projectCreationHandler,
'../Project/ProjectDeleter': this.projectDeleter,
'../Project/ProjectRootDocManager': this.rootDocManager,
'../Uploads/FileTypeManager': this.FileTypeManager,
'../Cooldown/CooldownManager': this.CooldownManager,
'../Uploads/FileTypeManager': this.FileTypeManager,
'../../infrastructure/Modules': this.Modules,
'../Notifications/NotificationsBuilder': this.NotificationsBuilder,
'../Project/ProjectCreationHandler': this.ProjectCreationHandler,
'../Project/ProjectDeleter': this.ProjectDeleter,
'../Project/ProjectGetter': this.ProjectGetter,
'../Project/ProjectHelper': this.ProjectHelper,
'../Project/ProjectRootDocManager': this.RootDocManager,
'./UpdateMerger': this.UpdateMerger,
'logger-sharelatex': {
log() {}
debug() {}
}
}
})
this.user_id = 'dsad29jlkjas'
return (this.source = 'dropbox')
})
describe('getting an update', function() {
it('should send the update to the update merger', function(done) {
const path = '/path/here'
const update = {}
this.updateMerger.mergeUpdate = sinon.stub()
this.updateMerger.mergeUpdate
.withArgs(this.user_id, this.project_id, path, update, this.source)
.callsArg(5)
return this.handler.newUpdate(
this.user_id,
this.project.name,
path,
update,
this.source,
() => {
this.projectCreationHandler.createBlankProject.called.should.equal(
false
)
return done()
}
)
describe('with no matching project', function() {
setupMatchingProjects([])
receiveUpdate()
expectProjectCreated()
expectUpdateProcessed()
})
it('should create a new project if one does not already exit', function(done) {
this.projectLocator.findUsersProjectByName = sinon
.stub()
.callsArgWith(2, null, { project: null })
const path = '/'
return this.handler.newUpdate(
this.user_id,
this.project.name,
path,
{},
this.source,
() => {
this.projectCreationHandler.createBlankProject
.calledWith(this.user_id, this.project.name)
.should.equal(true)
return done()
}
)
describe('with one matching active project', function() {
setupMatchingProjects(['active1'])
receiveUpdate()
expectProjectNotCreated()
expectUpdateProcessed()
})
it('should not create a new project if one with the same name is archived', function(done) {
this.projectLocator.findUsersProjectByName = sinon
.stub()
.callsArgWith(2, null, {
project: this.project,
isArchivedOrTrashed: true
})
const path = '/'
return this.handler.newUpdate(
this.user_id,
this.project.name,
path,
{},
this.source,
err => {
expect(err).to.exist
expect(err.name).to.equal('ProjectIsArchivedOrTrashedError')
this.projectCreationHandler.createBlankProject
.calledWith(this.user_id, this.project.name)
.should.equal(false)
return done()
}
)
describe('with one matching archived project', function() {
setupMatchingProjects(['archived1'])
receiveUpdate()
expectProjectNotCreated()
expectUpdateNotProcessed()
expectDropboxNotUnlinked()
})
it('should not create a new project if one is found', function(done) {
this.projectLocator.findUsersProjectByName = sinon
.stub()
.callsArgWith(2, null, {
project: this.project,
isArchivedOrTrashed: false
})
const path = '/'
return this.handler.newUpdate(
this.user_id,
this.project.name,
path,
{},
this.source,
err => {
expect(err).to.not.exist
this.projectCreationHandler.createBlankProject
.calledWith(this.user_id, this.project.name)
.should.equal(false)
return done()
}
)
describe('with two matching active projects', function() {
setupMatchingProjects(['active1', 'active2'])
receiveUpdate()
expectProjectNotCreated()
expectUpdateNotProcessed()
expectDropboxUnlinked()
})
it('should set the root doc automatically if a new project is created', function(done) {
this.projectLocator.findUsersProjectByName = sinon
.stub()
.callsArgWith(2, null, { project: null })
this.handler._rootDocTimeoutLength = 0
const path = '/'
return this.handler.newUpdate(
this.user_id,
this.project.name,
path,
{},
this.source,
() => {
return setTimeout(() => {
this.rootDocManager.setRootDocAutomatically
.calledWith(this.project._id)
.should.equal(true)
return done()
}, 1)
}
)
describe('with two matching archived projects', function() {
setupMatchingProjects(['archived1', 'archived2'])
receiveUpdate()
expectProjectNotCreated()
expectUpdateNotProcessed()
expectDropboxNotUnlinked()
})
it('should not update files that should be ignored', function(done) {
this.FileTypeManager.shouldIgnore = sinon
.stub()
.callsArgWith(1, null, true)
this.projectLocator.findUsersProjectByName = sinon
.stub()
.callsArgWith(2, null, { project: null })
const path = '/.gitignore'
this.updateMerger.mergeUpdate = sinon.stub()
return this.handler.newUpdate(
this.user_id,
this.project.name,
path,
{},
this.source,
() => {
this.updateMerger.mergeUpdate.called.should.equal(false)
return done()
}
)
describe('with one matching active and one matching archived project', function() {
setupMatchingProjects(['active1', 'archived1'])
receiveUpdate()
expectProjectNotCreated()
expectUpdateNotProcessed()
expectDropboxUnlinked()
})
it('should check if the project is on cooldown', function(done) {
this.CooldownManager.isProjectOnCooldown = sinon
.stub()
.callsArgWith(1, null, false)
this.projectLocator.findUsersProjectByName = sinon
.stub()
.callsArgWith(2, null, { project: null })
const path = '/path/here'
const update = {}
this.updateMerger.mergeUpdate = sinon.stub()
this.updateMerger.mergeUpdate
.withArgs(this.user_id, this.project_id, path, update, this.source)
.callsArg(5)
return this.handler.newUpdate(
this.user_id,
this.project.name,
path,
update,
this.source,
err => {
expect(err).to.be.oneOf([null, undefined])
this.CooldownManager.isProjectOnCooldown.callCount.should.equal(1)
this.CooldownManager.isProjectOnCooldown
.calledWith(this.project_id)
.should.equal(true)
this.FileTypeManager.shouldIgnore.callCount.should.equal(1)
this.updateMerger.mergeUpdate.callCount.should.equal(1)
return done()
}
)
describe('update to a file that should be ignored', function(done) {
setupMatchingProjects(['active1'])
beforeEach(function() {
this.FileTypeManager.shouldIgnore.yields(null, true)
})
receiveUpdate()
expectProjectNotCreated()
expectUpdateNotProcessed()
expectDropboxNotUnlinked()
})
it('should return error and not proceed with update if project is on cooldown', function(done) {
this.CooldownManager.isProjectOnCooldown = sinon
.stub()
.callsArgWith(1, null, true)
this.projectLocator.findUsersProjectByName = sinon
.stub()
.callsArgWith(2, null, { project: null })
this.FileTypeManager.shouldIgnore = sinon
.stub()
.callsArgWith(1, null, false)
const path = '/path/here'
const update = {}
this.updateMerger.mergeUpdate = sinon.stub()
this.updateMerger.mergeUpdate
.withArgs(this.user_id, this.project_id, path, update, this.source)
.callsArg(5)
return this.handler.newUpdate(
this.user_id,
this.project.name,
path,
update,
this.source,
err => {
expect(err).to.not.be.oneOf([null, undefined])
expect(err).to.be.instanceof(Error)
this.CooldownManager.isProjectOnCooldown.callCount.should.equal(1)
this.CooldownManager.isProjectOnCooldown
.calledWith(this.project_id)
.should.equal(true)
this.FileTypeManager.shouldIgnore.callCount.should.equal(0)
this.updateMerger.mergeUpdate.callCount.should.equal(0)
return done()
}
)
describe('update to a project on cooldown', function(done) {
setupMatchingProjects(['active1'])
setupProjectOnCooldown()
beforeEach(function(done) {
this.TpdsUpdateHandler.newUpdate(
this.userId,
this.projectName,
this.path,
this.update,
this.source,
err => {
expect(err).to.be.instanceof(Errors.TooManyRequestsError)
done()
}
)
})
expectUpdateNotProcessed()
})
})
describe('getting a delete :', function() {
it('should call deleteEntity in the collaberation manager', function(done) {
const path = '/delete/this'
const update = {}
this.updateMerger.deleteUpdate = sinon.stub().callsArg(4)
return this.handler.deleteUpdate(
this.user_id,
this.project.name,
path,
this.source,
() => {
this.projectDeleter.markAsDeletedByExternalSource
.calledWith(this.project._id)
.should.equal(false)
this.updateMerger.deleteUpdate
.calledWith(this.user_id, this.project_id, path, this.source)
.should.equal(true)
return done()
}
)
describe('getting a file delete', function() {
describe('with no matching project', function() {
setupMatchingProjects([])
receiveFileDelete()
expectDeleteNotProcessed()
expectProjectNotDeleted()
})
it('should mark the project as deleted by external source if path is a single slash', function(done) {
const path = '/'
return this.handler.deleteUpdate(
this.user_id,
this.project.name,
path,
this.source,
() => {
this.projectDeleter.markAsDeletedByExternalSource
.calledWith(this.project._id)
.should.equal(true)
return done()
}
)
describe('with one matching active project', function() {
setupMatchingProjects(['active1'])
receiveFileDelete()
expectDeleteProcessed()
expectProjectNotDeleted()
})
describe('with one matching archived project', function() {
setupMatchingProjects(['archived1'])
receiveFileDelete()
expectDeleteNotProcessed()
expectProjectNotDeleted()
})
describe('with two matching active projects', function() {
setupMatchingProjects(['active1', 'active2'])
receiveFileDelete()
expectDeleteNotProcessed()
expectProjectNotDeleted()
expectDropboxUnlinked()
})
describe('with two matching archived projects', function() {
setupMatchingProjects(['archived1', 'archived2'])
receiveFileDelete()
expectDeleteNotProcessed()
expectProjectNotDeleted()
expectDropboxNotUnlinked()
})
describe('with one matching active and one matching archived project', function() {
setupMatchingProjects(['active1', 'archived1'])
receiveFileDelete()
expectDeleteNotProcessed()
expectProjectNotDeleted()
expectDropboxUnlinked()
})
})
describe('getting a project delete', function() {
describe('with no matching project', function() {
setupMatchingProjects([])
receiveProjectDelete()
expectDeleteNotProcessed()
expectProjectNotDeleted()
})
describe('with one matching active project', function() {
setupMatchingProjects(['active1'])
receiveProjectDelete()
expectDeleteNotProcessed()
expectProjectDeleted()
})
describe('with one matching archived project', function() {
setupMatchingProjects(['archived1'])
receiveProjectDelete()
expectDeleteNotProcessed()
expectProjectNotDeleted()
})
describe('with two matching active projects', function() {
setupMatchingProjects(['active1', 'active2'])
receiveProjectDelete()
expectDeleteNotProcessed()
expectProjectNotDeleted()
expectDropboxUnlinked()
})
describe('with two matching archived projects', function() {
setupMatchingProjects(['archived1', 'archived2'])
receiveProjectDelete()
expectDeleteNotProcessed()
expectProjectNotDeleted()
expectDropboxNotUnlinked()
})
describe('with one matching active and one matching archived project', function() {
setupMatchingProjects(['active1', 'archived1'])
receiveProjectDelete()
expectDeleteNotProcessed()
expectProjectNotDeleted()
expectDropboxUnlinked()
})
})
})
/* Setup helpers */
function setupMatchingProjects(projectKeys) {
beforeEach(function() {
const projects = projectKeys.map(key => this.projects[key])
this.ProjectGetter.findUsersProjectsByName
.withArgs(this.userId, this.projectName)
.yields(null, projects)
})
}
function setupProjectOnCooldown() {
beforeEach(function() {
this.CooldownManager.isProjectOnCooldown
.withArgs(this.projects.active1._id)
.yields(null, true)
})
}
/* Test helpers */
function receiveUpdate() {
beforeEach(function(done) {
this.TpdsUpdateHandler.newUpdate(
this.userId,
this.projectName,
this.path,
this.update,
this.source,
done
)
})
}
function receiveFileDelete() {
beforeEach(function(done) {
this.TpdsUpdateHandler.deleteUpdate(
this.userId,
this.projectName,
this.path,
this.source,
done
)
})
}
function receiveProjectDelete() {
beforeEach(function(done) {
this.TpdsUpdateHandler.deleteUpdate(
this.userId,
this.projectName,
'/',
this.source,
done
)
})
}
/* Expectations */
function expectProjectCreated() {
it('creates a project', function() {
expect(
this.ProjectCreationHandler.createBlankProject
).to.have.been.calledWith(this.userId, this.projectName)
})
it('sets the root doc', function() {
// Fire pending timers
this.clock.runAll()
expect(this.RootDocManager.setRootDocAutomatically).to.have.been.calledWith(
this.projects.active1._id
)
})
}
function expectProjectNotCreated() {
it('does not create a project', function() {
expect(this.ProjectCreationHandler.createBlankProject).not.to.have.been
.called
})
it('does not set the root doc', function() {
// Fire pending timers
this.clock.runAll()
expect(this.RootDocManager.setRootDocAutomatically).not.to.have.been.called
})
}
function expectUpdateProcessed() {
it('processes the update', function() {
expect(this.UpdateMerger.mergeUpdate).to.have.been.calledWith(
this.userId,
this.projects.active1._id,
this.path,
this.update,
this.source
)
})
}
function expectUpdateNotProcessed() {
it('does not process the update', function() {
expect(this.UpdateMerger.mergeUpdate).not.to.have.been.called
})
}
function expectDropboxUnlinked() {
it('unlinks Dropbox', function() {
expect(this.Modules.hooks.fire).to.have.been.calledWith(
'removeDropbox',
this.userId
)
})
it('creates a notification that dropbox was unlinked', function() {
expect(
this.NotificationsBuilder.dropboxDuplicateProjectNames
).to.have.been.calledWith(this.userId)
expect(this.notification.create).to.have.been.calledWith(this.projectName)
})
}
function expectDropboxNotUnlinked() {
it('does not unlink Dropbox', function() {
expect(this.Modules.hooks.fire).not.to.have.been.called
})
it('does not create a notification that dropbox was unlinked', function() {
expect(this.NotificationsBuilder.dropboxDuplicateProjectNames).not.to.have
.been.called
})
}
function expectDeleteProcessed() {
it('processes the delete', function() {
expect(this.UpdateMerger.deleteUpdate).to.have.been.calledWith(
this.userId,
this.projects.active1._id,
this.path,
this.source
)
})
}
function expectDeleteNotProcessed() {
it('does not process the delete', function() {
expect(this.UpdateMerger.deleteUpdate).not.to.have.been.called
})
}
function expectProjectDeleted() {
it('deletes the project', function() {
expect(
this.ProjectDeleter.markAsDeletedByExternalSource
).to.have.been.calledWith(this.projects.active1._id)
})
}
function expectProjectNotDeleted() {
it('does not delete the project', function() {
expect(this.ProjectDeleter.markAsDeletedByExternalSource).not.to.have.been
.called
})
}