overleaf/services/web/test/unit/src/Project/ProjectDetailsHandlerTests.js

748 lines
23 KiB
JavaScript
Raw Normal View History

/* eslint-disable
handle-callback-err,
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:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const should = require('chai').should()
const modulePath = '../../../../app/src/Features/Project/ProjectDetailsHandler'
const Errors = require('../../../../app/src/Features/Errors/Errors')
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { assert } = require('chai')
const { expect } = require('chai')
require('chai').should()
describe('ProjectDetailsHandler', function() {
beforeEach(function() {
this.project_id = '321l3j1kjkjl'
this.user_id = 'user-id-123'
this.project = {
name: 'project',
description: 'this is a great project',
something: 'should not exist',
compiler: 'latexxxxxx',
owner_ref: this.user_id
}
this.user = { features: 'mock-features' }
this.ProjectGetter = {
getProjectWithoutDocLines: sinon
.stub()
.callsArgWith(1, null, this.project),
getProject: sinon.stub().callsArgWith(2, null, this.project)
}
this.ProjectModel = {
update: sinon.stub(),
findOne: sinon.stub()
}
this.UserGetter = { getUser: sinon.stub().callsArgWith(1, null, this.user) }
this.tpdsUpdateSender = { moveEntity: sinon.stub().callsArgWith(1) }
this.ProjectEntityHandler = {
flushProjectToThirdPartyDataStore: sinon.stub().callsArg(1)
}
this.CollaboratorsHandler = {
removeUserFromProject: sinon.stub().callsArg(2)
}
return (this.handler = SandboxedModule.require(modulePath, {
requires: {
'./ProjectGetter': this.ProjectGetter,
'../../models/Project': {
Project: this.ProjectModel
},
'../User/UserGetter': this.UserGetter,
'../ThirdPartyDataStore/TpdsUpdateSender': this.tpdsUpdateSender,
'./ProjectEntityHandler': this.ProjectEntityHandler,
'../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler,
'logger-sharelatex': {
log() {},
err() {}
},
'./ProjectTokenGenerator': (this.ProjectTokenGenerator = {}),
'settings-sharelatex': (this.settings = {
defaultFeatures: 'default-features'
})
}
}))
})
describe('getDetails', function() {
it('should find the project and owner', function(done) {
return this.handler.getDetails(this.project_id, (err, details) => {
details.name.should.equal(this.project.name)
details.description.should.equal(this.project.description)
details.compiler.should.equal(this.project.compiler)
details.features.should.equal(this.user.features)
assert.equal(details.something, undefined)
return done()
})
})
it('should find overleaf metadata if it exists', function(done) {
this.project.overleaf = { id: 'id' }
return this.handler.getDetails(this.project_id, (err, details) => {
details.overleaf.should.equal(this.project.overleaf)
assert.equal(details.something, undefined)
return done()
})
})
it('should return an error for a non-existent project', function(done) {
this.ProjectGetter.getProject.callsArg(2, null, null)
const err = new Errors.NotFoundError('project not found')
return this.handler.getDetails(
'0123456789012345678901234',
(error, details) => {
err.should.eql(error)
return done()
}
)
})
it('should return the default features if no owner found', function(done) {
this.UserGetter.getUser.callsArgWith(1, null, null)
return this.handler.getDetails(this.project_id, (err, details) => {
details.features.should.equal(this.settings.defaultFeatures)
return done()
})
})
return it('should return the error', function(done) {
const error = 'some error'
this.ProjectGetter.getProject.callsArgWith(2, error)
return this.handler.getDetails(this.project_id, err => {
err.should.equal(error)
return done()
})
})
})
describe('transferOwnership', function() {
beforeEach(function() {
this.handler.generateUniqueName = sinon
.stub()
.callsArgWith(2, null, 'teapot')
return this.ProjectModel.update.callsArgWith(2)
})
it("should return a not found error if the project can't be found", function(done) {
this.ProjectGetter.getProject.callsArgWith(2)
return this.handler.transferOwnership('abc', '123', function(err) {
err.should.exist
err.name.should.equal('NotFoundError')
return done()
})
})
it("should return a not found error if the user can't be found", function(done) {
this.ProjectGetter.getProject.callsArgWith(2)
return this.handler.transferOwnership('abc', '123', function(err) {
err.should.exist
err.name.should.equal('NotFoundError')
return done()
})
})
it('should return an error if user cannot be removed as collaborator ', function(done) {
const errorMessage = 'user-cannot-be-removed'
this.CollaboratorsHandler.removeUserFromProject.callsArgWith(
2,
errorMessage
)
return this.handler.transferOwnership('abc', '123', function(err) {
err.should.exist
err.should.equal(errorMessage)
return done()
})
})
it('should transfer ownership of the project', function(done) {
return this.handler.transferOwnership('abc', '123', () => {
sinon.assert.calledWith(
this.ProjectModel.update,
{ _id: 'abc' },
sinon.match({ $set: { name: 'teapot' } })
)
return done()
})
})
it("should remove the user from the project's collaborators", function(done) {
return this.handler.transferOwnership('abc', '123', () => {
sinon.assert.calledWith(
this.CollaboratorsHandler.removeUserFromProject,
'abc',
'123'
)
return done()
})
})
it('should flush the project to tpds', function(done) {
return this.handler.transferOwnership('abc', '123', () => {
sinon.assert.calledWith(
this.ProjectEntityHandler.flushProjectToThirdPartyDataStore,
'abc'
)
return done()
})
})
it('should generate a unique name for the project', function(done) {
return this.handler.transferOwnership('abc', '123', () => {
sinon.assert.calledWith(
this.handler.generateUniqueName,
'123',
this.project.name
)
return done()
})
})
return it('should append the supplied suffix to the project name, if passed', function(done) {
return this.handler.transferOwnership('abc', '123', ' wombat', () => {
sinon.assert.calledWith(
this.handler.generateUniqueName,
'123',
`${this.project.name} wombat`
)
return done()
})
})
})
describe('getProjectDescription', function() {
it('should make a call to mongo just for the description', function(done) {
this.ProjectGetter.getProject.callsArgWith(2)
return this.handler.getProjectDescription(
this.project_id,
(err, description) => {
this.ProjectGetter.getProject
.calledWith(this.project_id, { description: true })
.should.equal(true)
return done()
}
)
})
return it('should return what the mongo call returns', function(done) {
const err = 'error'
const description = 'cool project'
this.ProjectGetter.getProject.callsArgWith(2, err, { description })
return this.handler.getProjectDescription(
this.project_id,
(returnedErr, returnedDescription) => {
err.should.equal(returnedErr)
description.should.equal(returnedDescription)
return done()
}
)
})
})
describe('setProjectDescription', function() {
beforeEach(function() {
return (this.description = 'updated teh description')
})
return it('should update the project detials', function(done) {
this.ProjectModel.update.callsArgWith(2)
return this.handler.setProjectDescription(
this.project_id,
this.description,
() => {
this.ProjectModel.update
.calledWith(
{ _id: this.project_id },
{ description: this.description }
)
.should.equal(true)
return done()
}
)
})
})
describe('renameProject', function() {
beforeEach(function() {
this.handler.validateProjectName = sinon.stub().yields()
this.ProjectModel.update.callsArgWith(2)
return (this.newName = 'new name here')
})
it('should update the project with the new name', function(done) {
const newName = 'new name here'
return this.handler.renameProject(this.project_id, this.newName, () => {
this.ProjectModel.update
.calledWith({ _id: this.project_id }, { name: this.newName })
.should.equal(true)
return done()
})
})
it('should tell the tpdsUpdateSender', function(done) {
return this.handler.renameProject(this.project_id, this.newName, () => {
this.tpdsUpdateSender.moveEntity
.calledWith({
project_id: this.project_id,
project_name: this.project.name,
newProjectName: this.newName
})
.should.equal(true)
return done()
})
})
return it('should not do anything with an invalid name', function(done) {
this.handler.validateProjectName = sinon
.stub()
.yields(new Error('invalid name'))
return this.handler.renameProject(this.project_id, this.newName, () => {
this.tpdsUpdateSender.moveEntity.called.should.equal(false)
this.ProjectModel.update.called.should.equal(false)
return done()
})
})
})
describe('validateProjectName', function() {
it('should reject undefined names', function(done) {
return this.handler.validateProjectName(undefined, function(error) {
expect(error).to.exist
return done()
})
})
it('should reject empty names', function(done) {
return this.handler.validateProjectName('', function(error) {
expect(error).to.exist
return done()
})
})
it('should reject names with /s', function(done) {
return this.handler.validateProjectName('foo/bar', function(error) {
expect(error).to.exist
return done()
})
})
it('should reject names with \\s', function(done) {
return this.handler.validateProjectName('foo\\bar', function(error) {
expect(error).to.exist
return done()
})
})
it('should reject long names', function(done) {
return this.handler.validateProjectName(
new Array(1000).join('a'),
function(error) {
expect(error).to.exist
return done()
}
)
})
return it('should accept normal names', function(done) {
return this.handler.validateProjectName('foobar', function(error) {
expect(error).to.not.exist
return done()
})
})
})
describe('ensureProjectNameIsUnique', function() {
beforeEach(function() {
this.result = {
owned: [
{ _id: 1, name: 'name' },
{ _id: 2, name: 'name1' },
{ _id: 3, name: 'name11' },
{ _id: 100, name: 'numeric' }
],
readAndWrite: [{ _id: 4, name: 'name2' }, { _id: 5, name: 'name22' }],
readOnly: [{ _id: 6, name: 'name3' }, { _id: 7, name: 'name33' }],
tokenReadAndWrite: [
{ _id: 8, name: 'name4' },
{ _id: 9, name: 'name44' }
],
tokenReadOnly: [
{ _id: 10, name: 'name5' },
{ _id: 11, name: 'name55' },
{ _id: 12, name: 'x'.repeat(15) }
]
}
for (let i of Array.from(
[
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20
].concat([30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40])
)) {
this.result.owned.push({ _id: 100 + i, name: `numeric (${i})` })
}
return (this.ProjectGetter.findAllUsersProjects = sinon
.stub()
.callsArgWith(2, null, this.result))
})
it('should leave a unique name unchanged', function(done) {
return this.handler.ensureProjectNameIsUnique(
this.user_id,
'unique-name',
['-test-suffix'],
function(error, name, changed) {
expect(name).to.equal('unique-name')
expect(changed).to.equal(false)
return done()
}
)
})
it('should append a suffix to an existing name', function(done) {
return this.handler.ensureProjectNameIsUnique(
this.user_id,
'name1',
['-test-suffix'],
function(error, name, changed) {
expect(name).to.equal('name1-test-suffix')
expect(changed).to.equal(true)
return done()
}
)
})
it('should fallback to a second suffix when needed', function(done) {
return this.handler.ensureProjectNameIsUnique(
this.user_id,
'name1',
['1', '-test-suffix'],
function(error, name, changed) {
expect(name).to.equal('name1-test-suffix')
expect(changed).to.equal(true)
return done()
}
)
})
it('should truncate the name when append a suffix if the result is too long', function(done) {
this.handler.MAX_PROJECT_NAME_LENGTH = 20
return this.handler.ensureProjectNameIsUnique(
this.user_id,
'x'.repeat(15),
['-test-suffix'],
function(error, name, changed) {
expect(name).to.equal('x'.repeat(8) + '-test-suffix')
expect(changed).to.equal(true)
return done()
}
)
})
it('should use a numeric index if no suffix is supplied', function(done) {
return this.handler.ensureProjectNameIsUnique(
this.user_id,
'name1',
[],
function(error, name, changed) {
expect(name).to.equal('name1 (1)')
expect(changed).to.equal(true)
return done()
}
)
})
it('should use a numeric index if all suffixes are exhausted', function(done) {
return this.handler.ensureProjectNameIsUnique(
this.user_id,
'name',
['1', '11'],
function(error, name, changed) {
expect(name).to.equal('name (1)')
expect(changed).to.equal(true)
return done()
}
)
})
it('should find the next lowest available numeric index for the base name', function(done) {
return this.handler.ensureProjectNameIsUnique(
this.user_id,
'numeric',
[],
function(error, name, changed) {
expect(name).to.equal('numeric (21)')
expect(changed).to.equal(true)
return done()
}
)
})
it('should find the next available numeric index when a numeric index is already present', function(done) {
return this.handler.ensureProjectNameIsUnique(
this.user_id,
'numeric (5)',
[],
function(error, name, changed) {
expect(name).to.equal('numeric (21)')
expect(changed).to.equal(true)
return done()
}
)
})
return it('should not find a numeric index lower than the one already present', function(done) {
return this.handler.ensureProjectNameIsUnique(
this.user_id,
'numeric (31)',
[],
function(error, name, changed) {
expect(name).to.equal('numeric (41)')
expect(changed).to.equal(true)
return done()
}
)
})
})
describe('fixProjectName', function() {
it('should change empty names to Untitled', function() {
return expect(this.handler.fixProjectName('')).to.equal('Untitled')
})
it('should replace / with -', function() {
return expect(this.handler.fixProjectName('foo/bar')).to.equal('foo-bar')
})
it("should replace \\ with ''", function() {
return expect(this.handler.fixProjectName('foo \\ bar')).to.equal(
'foo bar'
)
})
it('should truncate long names', function() {
return expect(
this.handler.fixProjectName(new Array(1000).join('a'))
).to.equal('a'.repeat(150))
})
return it('should accept normal names', function() {
return expect(this.handler.fixProjectName('foobar')).to.equal('foobar')
})
})
describe('setPublicAccessLevel', function() {
beforeEach(function() {
this.ProjectModel.update.callsArgWith(2)
return (this.accessLevel = 'readOnly')
})
it('should update the project with the new level', function(done) {
return this.handler.setPublicAccessLevel(
this.project_id,
this.accessLevel,
() => {
this.ProjectModel.update
.calledWith(
{ _id: this.project_id },
{ publicAccesLevel: this.accessLevel }
)
.should.equal(true)
return done()
}
)
})
it('should not produce an error', function(done) {
return this.handler.setPublicAccessLevel(
this.project_id,
this.accessLevel,
err => {
expect(err).to.not.exist
return done()
}
)
})
return describe('when update produces an error', function() {
beforeEach(function() {
return this.ProjectModel.update.callsArgWith(2, new Error('woops'))
})
return it('should produce an error', function(done) {
return this.handler.setPublicAccessLevel(
this.project_id,
this.accessLevel,
err => {
expect(err).to.exist
expect(err).to.be.instanceof(Error)
return done()
}
)
})
})
})
return describe('ensureTokensArePresent', function() {
beforeEach(function() {})
describe('when the project has tokens', function() {
beforeEach(function() {
this.project = {
_id: this.project_id,
tokens: {
readOnly: 'aaa',
readAndWrite: '42bbb',
readAndWritePrefix: '42'
}
}
this.ProjectGetter.getProject = sinon
.stub()
.callsArgWith(2, null, this.project)
return (this.ProjectModel.update = sinon.stub())
})
it('should get the project', function(done) {
return this.handler.ensureTokensArePresent(
this.project_id,
(err, tokens) => {
expect(this.ProjectGetter.getProject.callCount).to.equal(1)
expect(
this.ProjectGetter.getProject.calledWith(this.project_id, {
tokens: 1
})
).to.equal(true)
return done()
}
)
})
it('should not update the project with new tokens', function(done) {
return this.handler.ensureTokensArePresent(
this.project_id,
(err, tokens) => {
expect(this.ProjectModel.update.callCount).to.equal(0)
return done()
}
)
})
return it('should produce the tokens without error', function(done) {
return this.handler.ensureTokensArePresent(
this.project_id,
(err, tokens) => {
expect(err).to.not.exist
expect(tokens).to.deep.equal(this.project.tokens)
return done()
}
)
})
})
return describe('when tokens are missing', function() {
beforeEach(function() {
this.project = { _id: this.project_id }
this.ProjectGetter.getProject = sinon
.stub()
.callsArgWith(2, null, this.project)
this.readOnlyToken = 'abc'
this.readAndWriteToken = '42def'
this.readAndWriteTokenPrefix = '42'
this.ProjectTokenGenerator.generateUniqueReadOnlyToken = sinon
.stub()
.callsArgWith(0, null, this.readOnlyToken)
this.ProjectTokenGenerator.readAndWriteToken = sinon.stub().returns({
token: this.readAndWriteToken,
numericPrefix: this.readAndWriteTokenPrefix
})
return (this.ProjectModel.update = sinon.stub().callsArgWith(2, null))
})
it('should get the project', function(done) {
return this.handler.ensureTokensArePresent(
this.project_id,
(err, tokens) => {
expect(this.ProjectGetter.getProject.callCount).to.equal(1)
expect(
this.ProjectGetter.getProject.calledWith(this.project_id, {
tokens: 1
})
).to.equal(true)
return done()
}
)
})
it('should update the project with new tokens', function(done) {
return this.handler.ensureTokensArePresent(
this.project_id,
(err, tokens) => {
expect(
this.ProjectTokenGenerator.generateUniqueReadOnlyToken.callCount
).to.equal(1)
expect(
this.ProjectTokenGenerator.readAndWriteToken.callCount
).to.equal(1)
expect(this.ProjectModel.update.callCount).to.equal(1)
expect(
this.ProjectModel.update.calledWith(
{ _id: this.project_id },
{
$set: {
tokens: {
readOnly: this.readOnlyToken,
readAndWrite: this.readAndWriteToken,
readAndWritePrefix: this.readAndWriteTokenPrefix
}
}
}
)
).to.equal(true)
return done()
}
)
})
return it('should produce the tokens without error', function(done) {
return this.handler.ensureTokensArePresent(
this.project_id,
(err, tokens) => {
expect(err).to.not.exist
expect(tokens).to.deep.equal({
readOnly: this.readOnlyToken,
readAndWrite: this.readAndWriteToken,
readAndWritePrefix: this.readAndWriteTokenPrefix
})
return done()
}
)
})
})
})
})