| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674 |
- import { vi, expect } from 'vitest'
- import sinon from 'sinon'
- import mongodb from 'mongodb-legacy'
- import Errors from '../../../../app/src/Features/Errors/Errors.js'
- import ProjectHelper from '../../../../app/src/Features/Project/ProjectHelper.js'
- const { ObjectId } = mongodb
- const MODULE_PATH = '../../../../app/src/Features/Project/ProjectDetailsHandler'
- vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
- vi.importActual('../../../../app/src/Features/Errors/Errors.js')
- )
- describe('ProjectDetailsHandler', function () {
- beforeEach(async function (ctx) {
- ctx.user = {
- _id: new ObjectId(),
- email: 'user@example.com',
- features: 'mock-features',
- }
- ctx.collaborator = {
- _id: new ObjectId(),
- email: 'collaborator@example.com',
- }
- ctx.project = {
- _id: new ObjectId(),
- name: 'project',
- description: 'this is a great project',
- something: 'should not exist',
- compiler: 'latexxxxxx',
- owner_ref: ctx.user._id,
- collaberator_refs: [ctx.collaborator._id],
- }
- ctx.ProjectGetter = {
- promises: {
- getProjectWithoutDocLines: sinon.stub().resolves(ctx.project),
- getProject: sinon.stub().resolves(ctx.project),
- findAllUsersProjects: sinon.stub().resolves({
- owned: [],
- readAndWrite: [],
- readOnly: [],
- tokenReadAndWrite: [],
- tokenReadOnly: [],
- }),
- },
- }
- ctx.ProjectModelUpdateQuery = {
- exec: sinon.stub().resolves(),
- }
- ctx.ProjectModel = {
- updateOne: sinon.stub().returns(ctx.ProjectModelUpdateQuery),
- }
- ctx.UserGetter = {
- promises: {
- getUser: sinon.stub().resolves(ctx.user),
- },
- }
- ctx.TpdsUpdateSender = {
- promises: {
- moveEntity: sinon.stub().resolves(),
- },
- }
- ctx.TokenGenerator = {
- readAndWriteToken: sinon.stub(),
- promises: {
- generateUniqueReadOnlyToken: sinon.stub(),
- },
- }
- ctx.settings = {
- defaultFeatures: 'default-features',
- }
- vi.doMock('../../../../app/src/Features/Project/ProjectHelper', () => ({
- default: ProjectHelper,
- }))
- vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
- default: ctx.ProjectGetter,
- }))
- vi.doMock('../../../../app/src/models/Project', () => ({
- Project: ctx.ProjectModel,
- }))
- vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
- default: ctx.UserGetter,
- }))
- vi.doMock(
- '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender',
- () => ({
- default: ctx.TpdsUpdateSender,
- })
- )
- vi.doMock(
- '../../../../app/src/Features/TokenGenerator/TokenGenerator',
- () => ({
- default: ctx.TokenGenerator,
- })
- )
- vi.doMock('@overleaf/settings', () => ({
- default: ctx.settings,
- }))
- ctx.handler = (await import(MODULE_PATH)).default
- })
- describe('getDetails', function () {
- it('should find the project and owner', async function (ctx) {
- const details = await ctx.handler.promises.getDetails(ctx.project._id)
- details.name.should.equal(ctx.project.name)
- details.description.should.equal(ctx.project.description)
- details.compiler.should.equal(ctx.project.compiler)
- details.features.should.equal(ctx.user.features)
- expect(details.something).to.be.undefined
- })
- it('should find overleaf metadata if it exists', async function (ctx) {
- ctx.project.overleaf = { id: 'id' }
- const details = await ctx.handler.promises.getDetails(ctx.project._id)
- details.overleaf.should.equal(ctx.project.overleaf)
- expect(details.something).to.be.undefined
- })
- it('should return an error for a non-existent project', async function (ctx) {
- ctx.ProjectGetter.promises.getProject.resolves(null)
- await expect(
- ctx.handler.promises.getDetails('0123456789012345678901234')
- ).to.be.rejectedWith(Errors.NotFoundError)
- })
- it('should return the default features if no owner found', async function (ctx) {
- ctx.UserGetter.promises.getUser.resolves(null)
- const details = await ctx.handler.promises.getDetails(ctx.project._id)
- details.features.should.equal(ctx.settings.defaultFeatures)
- })
- it('should rethrow any error', async function (ctx) {
- ctx.ProjectGetter.promises.getProject.rejects(new Error('boom'))
- await expect(ctx.handler.promises.getDetails(ctx.project._id)).to.be
- .rejected
- })
- })
- describe('getProjectDescription', function () {
- it('should make a call to mongo just for the description', async function (ctx) {
- ctx.ProjectGetter.promises.getProject.resolves()
- await ctx.handler.promises.getProjectDescription(ctx.project._id)
- expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledWith(
- ctx.project._id,
- { description: true }
- )
- })
- it('should return what the mongo call returns', async function (ctx) {
- const expectedDescription = 'cool project'
- ctx.ProjectGetter.promises.getProject.resolves({
- description: expectedDescription,
- })
- const description = await ctx.handler.promises.getProjectDescription(
- ctx.project._id
- )
- expect(description).to.equal(expectedDescription)
- })
- })
- describe('setProjectDescription', function () {
- beforeEach(function (ctx) {
- ctx.description = 'updated teh description'
- })
- it('should update the project detials', async function (ctx) {
- await ctx.handler.promises.setProjectDescription(
- ctx.project._id,
- ctx.description
- )
- expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
- { _id: ctx.project._id },
- { description: ctx.description }
- )
- })
- })
- describe('renameProject', function () {
- beforeEach(function (ctx) {
- ctx.newName = 'new name here'
- })
- it('should update the project with the new name', async function (ctx) {
- await ctx.handler.promises.renameProject(ctx.project._id, ctx.newName)
- expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
- { _id: ctx.project._id },
- { name: ctx.newName }
- )
- })
- it('should tell the TpdsUpdateSender', async function (ctx) {
- await ctx.handler.promises.renameProject(ctx.project._id, ctx.newName)
- expect(ctx.TpdsUpdateSender.promises.moveEntity).to.have.been.calledWith({
- projectId: ctx.project._id,
- projectName: ctx.project.name,
- newProjectName: ctx.newName,
- })
- })
- it('should not do anything with an invalid name', async function (ctx) {
- await expect(ctx.handler.promises.renameProject(ctx.project._id)).to.be
- .rejected
- expect(ctx.TpdsUpdateSender.promises.moveEntity).not.to.have.been.called
- expect(ctx.ProjectModel.updateOne).not.to.have.been.called
- })
- it('should trim whitespace around name', async function (ctx) {
- await ctx.handler.promises.renameProject(
- ctx.project._id,
- ` ${ctx.newName} `
- )
- expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
- { _id: ctx.project._id },
- { name: ctx.newName }
- )
- })
- })
- describe('validateProjectName', function () {
- it('should reject undefined names', async function (ctx) {
- await expect(ctx.handler.promises.validateProjectName(undefined)).to.be
- .rejected
- })
- it('should reject empty names', async function (ctx) {
- await expect(ctx.handler.promises.validateProjectName('')).to.be.rejected
- })
- it('should reject names with /s', async function (ctx) {
- await expect(ctx.handler.promises.validateProjectName('foo/bar')).to.be
- .rejected
- })
- it('should reject names with \\s', async function (ctx) {
- await expect(ctx.handler.promises.validateProjectName('foo\\bar')).to.be
- .rejected
- })
- it('should reject long names', async function (ctx) {
- await expect(ctx.handler.promises.validateProjectName('a'.repeat(1000)))
- .to.be.rejected
- })
- it('should accept normal names', async function (ctx) {
- await expect(ctx.handler.promises.validateProjectName('foobar')).to.be
- .fulfilled
- })
- })
- describe('generateUniqueName', function () {
- // actually testing `ProjectHelper.promises.ensureNameIsUnique()`
- beforeEach(function (ctx) {
- ctx.longName = 'x'.repeat(ctx.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: 143, 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: ctx.longName },
- ],
- }
- ctx.ProjectGetter.promises.findAllUsersProjects.resolves(usersProjects)
- })
- it('should leave a unique name unchanged', async function (ctx) {
- const name = await ctx.handler.promises.generateUniqueName(
- ctx.user._id,
- 'unique-name',
- ['-test-suffix']
- )
- expect(name).to.equal('unique-name')
- })
- it('should append a suffix to an existing name', async function (ctx) {
- const name = await ctx.handler.promises.generateUniqueName(
- ctx.user._id,
- 'name1',
- ['-test-suffix']
- )
- expect(name).to.equal('name1-test-suffix')
- })
- it('should fallback to a second suffix when needed', async function (ctx) {
- const name = await ctx.handler.promises.generateUniqueName(
- ctx.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 (ctx) {
- const name = await ctx.handler.promises.generateUniqueName(
- ctx.user._id,
- ctx.longName,
- ['-test-suffix']
- )
- expect(name).to.equal(
- ctx.longName.substr(0, ctx.handler.MAX_PROJECT_NAME_LENGTH - 12) +
- '-test-suffix'
- )
- })
- it('should use a numeric index if no suffix is supplied', async function (ctx) {
- const name = await ctx.handler.promises.generateUniqueName(
- ctx.user._id,
- 'name1',
- []
- )
- expect(name).to.equal('name1 (1)')
- })
- it('should use a numeric index if all suffixes are exhausted', async function (ctx) {
- const name = await ctx.handler.promises.generateUniqueName(
- ctx.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 (ctx) {
- const name = await ctx.handler.promises.generateUniqueName(
- ctx.user._id,
- 'numeric',
- []
- )
- expect(name).to.equal('numeric (21)')
- })
- it('should not find a numeric index lower than the one already present', async function (ctx) {
- const name = await ctx.handler.promises.generateUniqueName(
- ctx.user._id,
- 'numeric (31)',
- []
- )
- expect(name).to.equal('numeric (41)')
- })
- it('should handle years in name', async function (ctx) {
- const name = await ctx.handler.promises.generateUniqueName(
- ctx.user._id,
- 'unique-name (2021)',
- []
- )
- expect(name).to.equal('unique-name (2021)')
- })
- it('should handle duplicating with year in name', async function (ctx) {
- const name = await ctx.handler.promises.generateUniqueName(
- ctx.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 (ctx) {
- const name = await ctx.handler.promises.generateUniqueName(
- ctx.user._id,
- 'Resume (2020',
- []
- )
- expect(name).to.equal('Resume (2020 (1)')
- })
- it('should create the project with the provided name', async function (ctx) {
- const name = await ctx.handler.promises.generateUniqueName(
- ctx.user._id,
- 'Yearbook (2021',
- []
- )
- expect(name).to.equal('Yearbook (2021')
- })
- })
- describe('numeric index is already present', function () {
- describe('when there is 1 project "x (2)"', function () {
- beforeEach(function (ctx) {
- const usersProjects = {
- owned: [{ _id: 1, name: 'x (2)' }],
- }
- ctx.ProjectGetter.promises.findAllUsersProjects.resolves(
- usersProjects
- )
- })
- it('should produce "x (3)" uploading a zip with name "x (2)"', async function (ctx) {
- const name = await ctx.handler.promises.generateUniqueName(
- ctx.user._id,
- 'x (2)',
- []
- )
- expect(name).to.equal('x (3)')
- })
- })
- describe('when there are 2 projects "x (2)" and "x (3)"', function () {
- beforeEach(function (ctx) {
- const usersProjects = {
- owned: [
- { _id: 1, name: 'x (2)' },
- { _id: 2, name: 'x (3)' },
- ],
- }
- ctx.ProjectGetter.promises.findAllUsersProjects.resolves(
- usersProjects
- )
- })
- it('should produce "x (4)" when uploading a zip with name "x (2)"', async function (ctx) {
- const name = await ctx.handler.promises.generateUniqueName(
- ctx.user._id,
- 'x (2)',
- []
- )
- expect(name).to.equal('x (4)')
- })
- })
- describe('when there are 2 projects "x (2)" and "x (4)"', function () {
- beforeEach(function (ctx) {
- const usersProjects = {
- owned: [
- { _id: 1, name: 'x (2)' },
- { _id: 2, name: 'x (4)' },
- ],
- }
- ctx.ProjectGetter.promises.findAllUsersProjects.resolves(
- usersProjects
- )
- })
- it('should produce "x (3)" when uploading a zip with name "x (2)"', async function (ctx) {
- const name = await ctx.handler.promises.generateUniqueName(
- ctx.user._id,
- 'x (2)',
- []
- )
- expect(name).to.equal('x (3)')
- })
- it('should produce "x (5)" when uploading a zip with name "x (4)"', async function (ctx) {
- const name = await ctx.handler.promises.generateUniqueName(
- ctx.user._id,
- 'x (4)',
- []
- )
- expect(name).to.equal('x (5)')
- })
- })
- })
- })
- describe('fixProjectName', function () {
- it('should change empty names to Untitled', function (ctx) {
- expect(ctx.handler.fixProjectName('')).to.equal('Untitled')
- })
- it('should replace / with -', function (ctx) {
- expect(ctx.handler.fixProjectName('foo/bar')).to.equal('foo-bar')
- })
- it("should replace \\ with ''", function (ctx) {
- expect(ctx.handler.fixProjectName('foo \\ bar')).to.equal('foo bar')
- })
- it('should truncate long names', function (ctx) {
- expect(ctx.handler.fixProjectName('a'.repeat(1000))).to.equal(
- 'a'.repeat(150)
- )
- })
- it('should accept normal names', function (ctx) {
- expect(ctx.handler.fixProjectName('foobar')).to.equal('foobar')
- })
- it('should trim name after truncation', function (ctx) {
- expect(ctx.handler.fixProjectName('a'.repeat(149) + ' a')).to.equal(
- 'a'.repeat(149)
- )
- })
- })
- describe('setPublicAccessLevel', function () {
- beforeEach(function (ctx) {
- ctx.accessLevel = 'tokenBased'
- })
- it('should update the project with the new level', async function (ctx) {
- await ctx.handler.promises.setPublicAccessLevel(
- ctx.project._id,
- ctx.accessLevel
- )
- expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
- { _id: ctx.project._id },
- { publicAccesLevel: ctx.accessLevel }
- )
- })
- it('should not produce an error', async function (ctx) {
- await expect(
- ctx.handler.promises.setPublicAccessLevel(
- ctx.project._id,
- ctx.accessLevel
- )
- ).to.be.fulfilled
- })
- describe('when update produces an error', function () {
- beforeEach(function (ctx) {
- ctx.ProjectModelUpdateQuery.exec.rejects(new Error('woops'))
- })
- it('should produce an error', async function (ctx) {
- await expect(
- ctx.handler.promises.setPublicAccessLevel(
- ctx.project._id,
- ctx.accessLevel
- )
- ).to.be.rejected
- })
- })
- })
- describe('ensureTokensArePresent', function () {
- describe('when the project has tokens', function () {
- beforeEach(function (ctx) {
- ctx.project = {
- _id: ctx.project._id,
- tokens: {
- readOnly: 'aaa',
- readAndWrite: '42bbb',
- readAndWritePrefix: '42',
- },
- }
- ctx.ProjectGetter.promises.getProject.resolves(ctx.project)
- })
- it('should get the project', async function (ctx) {
- await ctx.handler.promises.ensureTokensArePresent(ctx.project._id)
- expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledOnce
- expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledWith(
- ctx.project._id,
- {
- tokens: 1,
- }
- )
- })
- it('should not update the project with new tokens', async function (ctx) {
- await ctx.handler.promises.ensureTokensArePresent(ctx.project._id)
- expect(ctx.ProjectModel.updateOne).not.to.have.been.called
- })
- })
- describe('when tokens are missing', function () {
- beforeEach(function (ctx) {
- ctx.project = { _id: ctx.project._id }
- ctx.ProjectGetter.promises.getProject.resolves(ctx.project)
- ctx.readOnlyToken = 'abc'
- ctx.readAndWriteToken = '42def'
- ctx.readAndWriteTokenPrefix = '42'
- ctx.TokenGenerator.promises.generateUniqueReadOnlyToken.resolves(
- ctx.readOnlyToken
- )
- ctx.TokenGenerator.readAndWriteToken.returns({
- token: ctx.readAndWriteToken,
- numericPrefix: ctx.readAndWriteTokenPrefix,
- })
- })
- it('should get the project', async function (ctx) {
- await ctx.handler.promises.ensureTokensArePresent(ctx.project._id)
- expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledOnce
- expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledWith(
- ctx.project._id,
- {
- tokens: 1,
- }
- )
- })
- it('should update the project with new tokens', async function (ctx) {
- await ctx.handler.promises.ensureTokensArePresent(ctx.project._id)
- expect(ctx.TokenGenerator.promises.generateUniqueReadOnlyToken).to.have
- .been.calledOnce
- expect(ctx.TokenGenerator.readAndWriteToken).to.have.been.calledOnce
- expect(ctx.ProjectModel.updateOne).to.have.been.calledOnce
- expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
- { _id: ctx.project._id },
- {
- $set: {
- tokens: {
- readOnly: ctx.readOnlyToken,
- readAndWrite: ctx.readAndWriteToken,
- readAndWritePrefix: ctx.readAndWriteTokenPrefix,
- },
- },
- }
- )
- })
- })
- })
- describe('clearTokens', function () {
- it('clears the tokens from the project', async function (ctx) {
- await ctx.handler.promises.clearTokens(ctx.project._id)
- expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
- { _id: ctx.project._id },
- { $unset: { tokens: 1 }, $set: { publicAccesLevel: 'private' } }
- )
- })
- })
- })
|