mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
e536ed1661
[web] Rename project: trim whitespace on ends GitOrigin-RevId: 2499d9e206ed5c929870a0f50cccd07ce3ec5ba7
592 lines
20 KiB
JavaScript
592 lines
20 KiB
JavaScript
const SandboxedModule = require('sandboxed-module')
|
|
const sinon = require('sinon')
|
|
const { expect } = require('chai')
|
|
const { ObjectId } = require('mongodb')
|
|
const Errors = require('../../../../app/src/Features/Errors/Errors')
|
|
const ProjectHelper = require('../../../../app/src/Features/Project/ProjectHelper')
|
|
|
|
const MODULE_PATH = '../../../../app/src/Features/Project/ProjectDetailsHandler'
|
|
|
|
describe('ProjectDetailsHandler', function () {
|
|
beforeEach(function () {
|
|
this.user = {
|
|
_id: ObjectId(),
|
|
email: 'user@example.com',
|
|
features: 'mock-features',
|
|
}
|
|
this.collaborator = {
|
|
_id: ObjectId(),
|
|
email: 'collaborator@example.com',
|
|
}
|
|
this.project = {
|
|
_id: ObjectId(),
|
|
name: 'project',
|
|
description: 'this is a great project',
|
|
something: 'should not exist',
|
|
compiler: 'latexxxxxx',
|
|
owner_ref: this.user._id,
|
|
collaberator_refs: [this.collaborator._id],
|
|
}
|
|
this.ProjectGetter = {
|
|
promises: {
|
|
getProjectWithoutDocLines: sinon.stub().resolves(this.project),
|
|
getProject: sinon.stub().resolves(this.project),
|
|
findAllUsersProjects: sinon.stub().resolves({
|
|
owned: [],
|
|
readAndWrite: [],
|
|
readOnly: [],
|
|
tokenReadAndWrite: [],
|
|
tokenReadOnly: [],
|
|
}),
|
|
},
|
|
}
|
|
this.ProjectModelUpdateQuery = {
|
|
exec: sinon.stub().resolves(),
|
|
}
|
|
this.ProjectModel = {
|
|
updateOne: sinon.stub().returns(this.ProjectModelUpdateQuery),
|
|
}
|
|
this.UserGetter = {
|
|
promises: {
|
|
getUser: sinon.stub().resolves(this.user),
|
|
},
|
|
}
|
|
this.TpdsUpdateSender = {
|
|
promises: {
|
|
moveEntity: sinon.stub().resolves(),
|
|
},
|
|
}
|
|
this.TokenGenerator = {
|
|
readAndWriteToken: sinon.stub(),
|
|
promises: {
|
|
generateUniqueReadOnlyToken: sinon.stub(),
|
|
},
|
|
}
|
|
this.settings = {
|
|
defaultFeatures: 'default-features',
|
|
}
|
|
this.handler = SandboxedModule.require(MODULE_PATH, {
|
|
requires: {
|
|
'./ProjectHelper': ProjectHelper,
|
|
'./ProjectGetter': this.ProjectGetter,
|
|
'../../models/Project': {
|
|
Project: this.ProjectModel,
|
|
},
|
|
'../User/UserGetter': this.UserGetter,
|
|
'../ThirdPartyDataStore/TpdsUpdateSender': this.TpdsUpdateSender,
|
|
'../TokenGenerator/TokenGenerator': this.TokenGenerator,
|
|
'@overleaf/settings': this.settings,
|
|
},
|
|
})
|
|
})
|
|
|
|
describe('getDetails', function () {
|
|
it('should find the project and owner', async function () {
|
|
const details = await this.handler.promises.getDetails(this.project._id)
|
|
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)
|
|
expect(details.something).to.be.undefined
|
|
})
|
|
|
|
it('should find overleaf metadata if it exists', async function () {
|
|
this.project.overleaf = { id: 'id' }
|
|
const details = await this.handler.promises.getDetails(this.project._id)
|
|
details.overleaf.should.equal(this.project.overleaf)
|
|
expect(details.something).to.be.undefined
|
|
})
|
|
|
|
it('should return an error for a non-existent project', async function () {
|
|
this.ProjectGetter.promises.getProject.resolves(null)
|
|
await expect(
|
|
this.handler.promises.getDetails('0123456789012345678901234')
|
|
).to.be.rejectedWith(Errors.NotFoundError)
|
|
})
|
|
|
|
it('should return the default features if no owner found', async function () {
|
|
this.UserGetter.promises.getUser.resolves(null)
|
|
const details = await this.handler.promises.getDetails(this.project._id)
|
|
details.features.should.equal(this.settings.defaultFeatures)
|
|
})
|
|
|
|
it('should rethrow any error', async function () {
|
|
this.ProjectGetter.promises.getProject.rejects(new Error('boom'))
|
|
await expect(this.handler.promises.getDetails(this.project._id)).to.be
|
|
.rejected
|
|
})
|
|
})
|
|
|
|
describe('getProjectDescription', function () {
|
|
it('should make a call to mongo just for the description', async function () {
|
|
this.ProjectGetter.promises.getProject.resolves()
|
|
await this.handler.promises.getProjectDescription(this.project._id)
|
|
expect(this.ProjectGetter.promises.getProject).to.have.been.calledWith(
|
|
this.project._id,
|
|
{ description: true }
|
|
)
|
|
})
|
|
|
|
it('should return what the mongo call returns', async function () {
|
|
const expectedDescription = 'cool project'
|
|
this.ProjectGetter.promises.getProject.resolves({
|
|
description: expectedDescription,
|
|
})
|
|
const description = await this.handler.promises.getProjectDescription(
|
|
this.project._id
|
|
)
|
|
expect(description).to.equal(expectedDescription)
|
|
})
|
|
})
|
|
|
|
describe('setProjectDescription', function () {
|
|
beforeEach(function () {
|
|
this.description = 'updated teh description'
|
|
})
|
|
|
|
it('should update the project detials', async function () {
|
|
await this.handler.promises.setProjectDescription(
|
|
this.project._id,
|
|
this.description
|
|
)
|
|
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
|
|
{ _id: this.project._id },
|
|
{ description: this.description }
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('renameProject', function () {
|
|
beforeEach(function () {
|
|
this.newName = 'new name here'
|
|
})
|
|
|
|
it('should update the project with the new name', async function () {
|
|
await this.handler.promises.renameProject(this.project._id, this.newName)
|
|
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
|
|
{ _id: this.project._id },
|
|
{ name: this.newName }
|
|
)
|
|
})
|
|
|
|
it('should tell the TpdsUpdateSender', async function () {
|
|
await this.handler.promises.renameProject(this.project._id, this.newName)
|
|
expect(this.TpdsUpdateSender.promises.moveEntity).to.have.been.calledWith(
|
|
{
|
|
project_id: this.project._id,
|
|
project_name: this.project.name,
|
|
newProjectName: this.newName,
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should not do anything with an invalid name', async function () {
|
|
await expect(this.handler.promises.renameProject(this.project._id)).to.be
|
|
.rejected
|
|
expect(this.TpdsUpdateSender.promises.moveEntity).not.to.have.been.called
|
|
expect(this.ProjectModel.updateOne).not.to.have.been.called
|
|
})
|
|
|
|
it('should trim whitespace around name', async function () {
|
|
await this.handler.promises.renameProject(
|
|
this.project._id,
|
|
` ${this.newName} `
|
|
)
|
|
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
|
|
{ _id: this.project._id },
|
|
{ name: this.newName }
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('validateProjectName', function () {
|
|
it('should reject undefined names', async function () {
|
|
await expect(this.handler.promises.validateProjectName(undefined)).to.be
|
|
.rejected
|
|
})
|
|
|
|
it('should reject empty names', async function () {
|
|
await expect(this.handler.promises.validateProjectName('')).to.be.rejected
|
|
})
|
|
|
|
it('should reject names with /s', async function () {
|
|
await expect(this.handler.promises.validateProjectName('foo/bar')).to.be
|
|
.rejected
|
|
})
|
|
|
|
it('should reject names with \\s', async function () {
|
|
await expect(this.handler.promises.validateProjectName('foo\\bar')).to.be
|
|
.rejected
|
|
})
|
|
|
|
it('should reject long names', async function () {
|
|
await expect(this.handler.promises.validateProjectName('a'.repeat(1000)))
|
|
.to.be.rejected
|
|
})
|
|
|
|
it('should accept normal names', async function () {
|
|
await expect(this.handler.promises.validateProjectName('foobar')).to.be
|
|
.fulfilled
|
|
})
|
|
})
|
|
|
|
describe('generateUniqueName', function () {
|
|
// actually testing `ProjectHelper.promises.ensureNameIsUnique()`
|
|
beforeEach(function () {
|
|
this.longName = 'x'.repeat(this.handler.MAX_PROJECT_NAME_LENGTH - 5)
|
|
const usersProjects = {
|
|
owned: [
|
|
{ _id: 1, name: 'name' },
|
|
{ _id: 2, name: 'name1' },
|
|
{ _id: 3, name: 'name11' },
|
|
{ _id: 100, name: 'numeric' },
|
|
{ _id: 101, name: 'numeric (1)' },
|
|
{ _id: 102, name: 'numeric (2)' },
|
|
{ _id: 103, name: 'numeric (3)' },
|
|
{ _id: 104, name: 'numeric (4)' },
|
|
{ _id: 105, name: 'numeric (5)' },
|
|
{ _id: 106, name: 'numeric (6)' },
|
|
{ _id: 107, name: 'numeric (7)' },
|
|
{ _id: 108, name: 'numeric (8)' },
|
|
{ _id: 109, name: 'numeric (9)' },
|
|
{ _id: 110, name: 'numeric (10)' },
|
|
{ _id: 111, name: 'numeric (11)' },
|
|
{ _id: 112, name: 'numeric (12)' },
|
|
{ _id: 113, name: 'numeric (13)' },
|
|
{ _id: 114, name: 'numeric (14)' },
|
|
{ _id: 115, name: 'numeric (15)' },
|
|
{ _id: 116, name: 'numeric (16)' },
|
|
{ _id: 117, name: 'numeric (17)' },
|
|
{ _id: 118, name: 'numeric (18)' },
|
|
{ _id: 119, name: 'numeric (19)' },
|
|
{ _id: 120, name: 'numeric (20)' },
|
|
{ _id: 130, name: 'numeric (30)' },
|
|
{ _id: 131, name: 'numeric (31)' },
|
|
{ _id: 132, name: 'numeric (32)' },
|
|
{ _id: 133, name: 'numeric (33)' },
|
|
{ _id: 134, name: 'numeric (34)' },
|
|
{ _id: 135, name: 'numeric (35)' },
|
|
{ _id: 136, name: 'numeric (36)' },
|
|
{ _id: 137, name: 'numeric (37)' },
|
|
{ _id: 138, name: 'numeric (38)' },
|
|
{ _id: 139, name: 'numeric (39)' },
|
|
{ _id: 140, name: 'numeric (40)' },
|
|
{ _id: 141, name: 'Yearbook (2021)' },
|
|
{ _id: 142, name: 'Yearbook (2021) (1)' },
|
|
{ _id: 142, name: 'Resume (2020' },
|
|
],
|
|
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: this.longName },
|
|
],
|
|
}
|
|
this.ProjectGetter.promises.findAllUsersProjects.resolves(usersProjects)
|
|
})
|
|
|
|
it('should leave a unique name unchanged', async function () {
|
|
const name = await this.handler.promises.generateUniqueName(
|
|
this.user._id,
|
|
'unique-name',
|
|
['-test-suffix']
|
|
)
|
|
expect(name).to.equal('unique-name')
|
|
})
|
|
|
|
it('should append a suffix to an existing name', async function () {
|
|
const name = await this.handler.promises.generateUniqueName(
|
|
this.user._id,
|
|
'name1',
|
|
['-test-suffix']
|
|
)
|
|
expect(name).to.equal('name1-test-suffix')
|
|
})
|
|
|
|
it('should fallback to a second suffix when needed', async function () {
|
|
const name = await this.handler.promises.generateUniqueName(
|
|
this.user._id,
|
|
'name1',
|
|
['1', '-test-suffix']
|
|
)
|
|
expect(name).to.equal('name1-test-suffix')
|
|
})
|
|
|
|
it('should truncate the name when append a suffix if the result is too long', async function () {
|
|
const name = await this.handler.promises.generateUniqueName(
|
|
this.user._id,
|
|
this.longName,
|
|
['-test-suffix']
|
|
)
|
|
expect(name).to.equal(
|
|
this.longName.substr(0, this.handler.MAX_PROJECT_NAME_LENGTH - 12) +
|
|
'-test-suffix'
|
|
)
|
|
})
|
|
|
|
it('should use a numeric index if no suffix is supplied', async function () {
|
|
const name = await this.handler.promises.generateUniqueName(
|
|
this.user._id,
|
|
'name1',
|
|
[]
|
|
)
|
|
expect(name).to.equal('name1 (1)')
|
|
})
|
|
|
|
it('should use a numeric index if all suffixes are exhausted', async function () {
|
|
const name = await this.handler.promises.generateUniqueName(
|
|
this.user._id,
|
|
'name',
|
|
['1', '11']
|
|
)
|
|
expect(name).to.equal('name (1)')
|
|
})
|
|
|
|
it('should find the next lowest available numeric index for the base name', async function () {
|
|
const name = await this.handler.promises.generateUniqueName(
|
|
this.user._id,
|
|
'numeric',
|
|
[]
|
|
)
|
|
expect(name).to.equal('numeric (21)')
|
|
})
|
|
|
|
it('should find the next available numeric index when a numeric index is already present', async function () {
|
|
const name = await this.handler.promises.generateUniqueName(
|
|
this.user._id,
|
|
'numeric (5)',
|
|
[]
|
|
)
|
|
expect(name).to.equal('numeric (21)')
|
|
})
|
|
|
|
it('should not find a numeric index lower than the one already present', async function () {
|
|
const name = await this.handler.promises.generateUniqueName(
|
|
this.user._id,
|
|
'numeric (31)',
|
|
[]
|
|
)
|
|
expect(name).to.equal('numeric (41)')
|
|
})
|
|
|
|
it('should handle years in name', async function () {
|
|
const name = await this.handler.promises.generateUniqueName(
|
|
this.user._id,
|
|
'unique-name (2021)',
|
|
[]
|
|
)
|
|
expect(name).to.equal('unique-name (2021)')
|
|
})
|
|
|
|
it('should handle duplicating with year in name', async function () {
|
|
const name = await this.handler.promises.generateUniqueName(
|
|
this.user._id,
|
|
'Yearbook (2021)',
|
|
[]
|
|
)
|
|
expect(name).to.equal('Yearbook (2021) (2)')
|
|
})
|
|
describe('title with that causes invalid regex', function () {
|
|
it('should create the project with a suffix when project name exists', async function () {
|
|
const name = await this.handler.promises.generateUniqueName(
|
|
this.user._id,
|
|
'Resume (2020',
|
|
[]
|
|
)
|
|
expect(name).to.equal('Resume (2020 (1)')
|
|
})
|
|
it('should create the project with the provided name', async function () {
|
|
const name = await this.handler.promises.generateUniqueName(
|
|
this.user._id,
|
|
'Yearbook (2021',
|
|
[]
|
|
)
|
|
expect(name).to.equal('Yearbook (2021')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('fixProjectName', function () {
|
|
it('should change empty names to Untitled', function () {
|
|
expect(this.handler.fixProjectName('')).to.equal('Untitled')
|
|
})
|
|
|
|
it('should replace / with -', function () {
|
|
expect(this.handler.fixProjectName('foo/bar')).to.equal('foo-bar')
|
|
})
|
|
|
|
it("should replace \\ with ''", function () {
|
|
expect(this.handler.fixProjectName('foo \\ bar')).to.equal('foo bar')
|
|
})
|
|
|
|
it('should truncate long names', function () {
|
|
expect(this.handler.fixProjectName('a'.repeat(1000))).to.equal(
|
|
'a'.repeat(150)
|
|
)
|
|
})
|
|
|
|
it('should accept normal names', function () {
|
|
expect(this.handler.fixProjectName('foobar')).to.equal('foobar')
|
|
})
|
|
})
|
|
|
|
describe('setPublicAccessLevel', function () {
|
|
beforeEach(function () {
|
|
this.accessLevel = 'readOnly'
|
|
})
|
|
|
|
it('should update the project with the new level', async function () {
|
|
await this.handler.promises.setPublicAccessLevel(
|
|
this.project._id,
|
|
this.accessLevel
|
|
)
|
|
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
|
|
{ _id: this.project._id },
|
|
{ publicAccesLevel: this.accessLevel }
|
|
)
|
|
})
|
|
|
|
it('should not produce an error', async function () {
|
|
await expect(
|
|
this.handler.promises.setPublicAccessLevel(
|
|
this.project._id,
|
|
this.accessLevel
|
|
)
|
|
).to.be.fulfilled
|
|
})
|
|
|
|
describe('when update produces an error', function () {
|
|
beforeEach(function () {
|
|
this.ProjectModelUpdateQuery.exec.rejects(new Error('woops'))
|
|
})
|
|
|
|
it('should produce an error', async function () {
|
|
await expect(
|
|
this.handler.promises.setPublicAccessLevel(
|
|
this.project._id,
|
|
this.accessLevel
|
|
)
|
|
).to.be.rejected
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('ensureTokensArePresent', 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.promises.getProject.resolves(this.project)
|
|
})
|
|
|
|
it('should get the project', async function () {
|
|
await this.handler.promises.ensureTokensArePresent(this.project._id)
|
|
expect(this.ProjectGetter.promises.getProject).to.have.been.calledOnce
|
|
expect(this.ProjectGetter.promises.getProject).to.have.been.calledWith(
|
|
this.project._id,
|
|
{
|
|
tokens: 1,
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should not update the project with new tokens', async function () {
|
|
await this.handler.promises.ensureTokensArePresent(this.project._id)
|
|
expect(this.ProjectModel.updateOne).not.to.have.been.called
|
|
})
|
|
|
|
it('should produce the tokens without error', async function () {
|
|
const tokens = await this.handler.promises.ensureTokensArePresent(
|
|
this.project._id
|
|
)
|
|
expect(tokens).to.deep.equal(this.project.tokens)
|
|
})
|
|
})
|
|
|
|
describe('when tokens are missing', function () {
|
|
beforeEach(function () {
|
|
this.project = { _id: this.project._id }
|
|
this.ProjectGetter.promises.getProject.resolves(this.project)
|
|
this.readOnlyToken = 'abc'
|
|
this.readAndWriteToken = '42def'
|
|
this.readAndWriteTokenPrefix = '42'
|
|
this.TokenGenerator.promises.generateUniqueReadOnlyToken.resolves(
|
|
this.readOnlyToken
|
|
)
|
|
this.TokenGenerator.readAndWriteToken.returns({
|
|
token: this.readAndWriteToken,
|
|
numericPrefix: this.readAndWriteTokenPrefix,
|
|
})
|
|
})
|
|
|
|
it('should get the project', async function () {
|
|
await this.handler.promises.ensureTokensArePresent(this.project._id)
|
|
expect(this.ProjectGetter.promises.getProject).to.have.been.calledOnce
|
|
expect(this.ProjectGetter.promises.getProject).to.have.been.calledWith(
|
|
this.project._id,
|
|
{
|
|
tokens: 1,
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should update the project with new tokens', async function () {
|
|
await this.handler.promises.ensureTokensArePresent(this.project._id)
|
|
expect(this.TokenGenerator.promises.generateUniqueReadOnlyToken).to.have
|
|
.been.calledOnce
|
|
expect(this.TokenGenerator.readAndWriteToken).to.have.been.calledOnce
|
|
expect(this.ProjectModel.updateOne).to.have.been.calledOnce
|
|
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
|
|
{ _id: this.project._id },
|
|
{
|
|
$set: {
|
|
tokens: {
|
|
readOnly: this.readOnlyToken,
|
|
readAndWrite: this.readAndWriteToken,
|
|
readAndWritePrefix: this.readAndWriteTokenPrefix,
|
|
},
|
|
},
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should produce the tokens without error', async function () {
|
|
const tokens = await this.handler.promises.ensureTokensArePresent(
|
|
this.project._id
|
|
)
|
|
expect(tokens).to.deep.equal({
|
|
readOnly: this.readOnlyToken,
|
|
readAndWrite: this.readAndWriteToken,
|
|
readAndWritePrefix: this.readAndWriteTokenPrefix,
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('clearTokens', function () {
|
|
it('clears the tokens from the project', async function () {
|
|
await this.handler.promises.clearTokens(this.project._id)
|
|
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
|
|
{ _id: this.project._id },
|
|
{ $unset: { tokens: 1 }, $set: { publicAccesLevel: 'private' } }
|
|
)
|
|
})
|
|
})
|
|
})
|