mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-02 13:49:00 +02:00
e76a8ff267
GitOrigin-RevId: 49404748cc90cb7bdef0460f7e9837196f81cae8
569 lines
16 KiB
JavaScript
569 lines
16 KiB
JavaScript
import { expect, vi } from 'vitest'
|
|
import mongodb from 'mongodb-legacy'
|
|
import sinon from 'sinon'
|
|
import Errors from '../../../../app/src/Features/Errors/Errors.js'
|
|
import MockResponse from '../helpers/MockResponse.js'
|
|
import MockRequest from '../helpers/MockRequest.js'
|
|
|
|
const ObjectId = mongodb.ObjectId
|
|
|
|
const MODULE_PATH =
|
|
'../../../../app/src/Features/ThirdPartyDataStore/TpdsController.mjs'
|
|
|
|
describe('TpdsController', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.metadata = {
|
|
projectId: new ObjectId(),
|
|
entityId: new ObjectId(),
|
|
folderId: new ObjectId(),
|
|
entityType: 'doc',
|
|
rev: 2,
|
|
}
|
|
ctx.TpdsUpdateHandler = {
|
|
promises: {
|
|
newUpdate: sinon.stub().resolves(ctx.metadata),
|
|
deleteUpdate: sinon.stub().resolves(ctx.metadata.entityId),
|
|
createFolder: sinon.stub().resolves(),
|
|
},
|
|
}
|
|
ctx.UpdateMerger = {
|
|
promises: {
|
|
mergeUpdate: sinon.stub().resolves(ctx.metadata),
|
|
deleteUpdate: sinon.stub().resolves(ctx.metadata.entityId),
|
|
},
|
|
}
|
|
ctx.NotificationsBuilder = {
|
|
tpdsFileLimit: sinon.stub().returns({ create: sinon.stub() }),
|
|
}
|
|
ctx.SessionManager = {
|
|
getLoggedInUserId: sinon.stub().returns('user-id'),
|
|
}
|
|
ctx.TpdsQueueManager = {
|
|
promises: {
|
|
getQueues: sinon.stub().resolves('queues'),
|
|
},
|
|
}
|
|
ctx.HttpErrorHandler = {
|
|
conflict: sinon.stub(),
|
|
}
|
|
|
|
ctx.newProject = { _id: new ObjectId() }
|
|
ctx.ProjectCreationHandler = {
|
|
promises: { createBlankProject: sinon.stub().resolves(ctx.newProject) },
|
|
}
|
|
ctx.ProjectDetailsHandler = {
|
|
promises: {
|
|
generateUniqueName: sinon.stub().resolves('unique'),
|
|
},
|
|
}
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateHandler',
|
|
() => ({
|
|
default: ctx.TpdsUpdateHandler,
|
|
})
|
|
)
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/ThirdPartyDataStore/UpdateMerger',
|
|
() => ({
|
|
default: ctx.UpdateMerger,
|
|
})
|
|
)
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Notifications/NotificationsBuilder',
|
|
() => ({
|
|
default: ctx.NotificationsBuilder,
|
|
})
|
|
)
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Authentication/SessionManager',
|
|
() => ({
|
|
default: ctx.SessionManager,
|
|
})
|
|
)
|
|
|
|
vi.doMock('../../../../app/src/Features/Errors/HttpErrorHandler', () => ({
|
|
default: ctx.HttpErrorHandler,
|
|
}))
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/ThirdPartyDataStore/TpdsQueueManager',
|
|
() => ({
|
|
default: ctx.TpdsQueueManager,
|
|
})
|
|
)
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Project/ProjectCreationHandler',
|
|
() => ({
|
|
default: ctx.ProjectCreationHandler,
|
|
})
|
|
)
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Project/ProjectDetailsHandler',
|
|
() => ({
|
|
default: ctx.ProjectDetailsHandler,
|
|
})
|
|
)
|
|
|
|
ctx.TpdsController = (await import(MODULE_PATH)).default
|
|
|
|
ctx.user_id = 'dsad29jlkjas'
|
|
})
|
|
|
|
describe('creating a project', function () {
|
|
it('should yield the new projects id', async function (ctx) {
|
|
await new Promise(resolve => {
|
|
const res = new MockResponse()
|
|
const req = new MockRequest()
|
|
req.params.user_id = ctx.user_id
|
|
req.body = { projectName: 'foo' }
|
|
res.callback = err => {
|
|
if (err) resolve(err)
|
|
expect(res.body).to.equal(
|
|
JSON.stringify({ projectId: ctx.newProject._id.toString() })
|
|
)
|
|
expect(
|
|
ctx.ProjectDetailsHandler.promises.generateUniqueName
|
|
).to.have.been.calledWith(ctx.user_id, 'foo')
|
|
expect(
|
|
ctx.ProjectCreationHandler.promises.createBlankProject
|
|
).to.have.been.calledWith(
|
|
ctx.user_id,
|
|
'unique',
|
|
{},
|
|
{ skipCreatingInTPDS: true }
|
|
)
|
|
resolve()
|
|
}
|
|
ctx.TpdsController.createProject(req, res)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('getting an update', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.projectName = 'projectName'
|
|
ctx.path = '/here.txt'
|
|
ctx.req = {
|
|
params: {
|
|
0: `${ctx.projectName}${ctx.path}`,
|
|
user_id: ctx.user_id,
|
|
project_id: '',
|
|
},
|
|
headers: {
|
|
'x-update-source': (ctx.source = 'dropbox'),
|
|
},
|
|
}
|
|
})
|
|
|
|
it('should process the update with the update receiver by name', async function (ctx) {
|
|
await new Promise(resolve => {
|
|
const res = {
|
|
json: payload => {
|
|
expect(payload).to.deep.equal({
|
|
status: 'applied',
|
|
projectId: ctx.metadata.projectId.toString(),
|
|
entityId: ctx.metadata.entityId.toString(),
|
|
folderId: ctx.metadata.folderId.toString(),
|
|
entityType: ctx.metadata.entityType,
|
|
rev: ctx.metadata.rev.toString(),
|
|
})
|
|
ctx.TpdsUpdateHandler.promises.newUpdate
|
|
.calledWith(
|
|
ctx.user_id,
|
|
'', // projectId
|
|
ctx.projectName,
|
|
ctx.path,
|
|
ctx.req,
|
|
ctx.source
|
|
)
|
|
.should.equal(true)
|
|
resolve()
|
|
},
|
|
}
|
|
ctx.TpdsController.mergeUpdate(ctx.req, res)
|
|
})
|
|
})
|
|
|
|
it('should indicate in the response when the update was rejected', async function (ctx) {
|
|
await new Promise(resolve => {
|
|
ctx.TpdsUpdateHandler.promises.newUpdate.resolves(null)
|
|
const res = {
|
|
json: payload => {
|
|
expect(payload).to.deep.equal({ status: 'rejected' })
|
|
resolve()
|
|
},
|
|
}
|
|
ctx.TpdsController.mergeUpdate(ctx.req, res)
|
|
})
|
|
})
|
|
|
|
it('should process the update with the update receiver by id', async function (ctx) {
|
|
await new Promise(resolve => {
|
|
const path = '/here.txt'
|
|
const req = {
|
|
pause() {},
|
|
params: { 0: path, user_id: ctx.user_id, project_id: '123' },
|
|
session: {
|
|
destroy() {},
|
|
},
|
|
headers: {
|
|
'x-update-source': (ctx.source = 'dropbox'),
|
|
},
|
|
}
|
|
const res = {
|
|
json: () => {
|
|
ctx.TpdsUpdateHandler.promises.newUpdate.should.have.been.calledWith(
|
|
ctx.user_id,
|
|
'123',
|
|
'', // projectName
|
|
'/here.txt',
|
|
req,
|
|
ctx.source
|
|
)
|
|
resolve()
|
|
},
|
|
}
|
|
ctx.TpdsController.mergeUpdate(req, res)
|
|
})
|
|
})
|
|
|
|
it('should return a 500 error when the update receiver fails', async function (ctx) {
|
|
await new Promise(resolve => {
|
|
ctx.TpdsUpdateHandler.promises.newUpdate.rejects(new Error())
|
|
const res = {
|
|
json: sinon.stub(),
|
|
}
|
|
ctx.TpdsController.mergeUpdate(ctx.req, res, err => {
|
|
expect(err).to.exist
|
|
expect(res.json).not.to.have.been.called
|
|
resolve()
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should return a 400 error when the project is too big', async function (ctx) {
|
|
await new Promise(resolve => {
|
|
ctx.TpdsUpdateHandler.promises.newUpdate.rejects({
|
|
message: 'project_has_too_many_files',
|
|
})
|
|
const res = {
|
|
sendStatus: status => {
|
|
expect(status).to.equal(400)
|
|
ctx.NotificationsBuilder.tpdsFileLimit.should.have.been.calledWith(
|
|
ctx.user_id
|
|
)
|
|
resolve()
|
|
},
|
|
}
|
|
ctx.TpdsController.mergeUpdate(ctx.req, res)
|
|
})
|
|
})
|
|
|
|
it('should return a 429 error when the update receiver fails due to too many requests error', async function (ctx) {
|
|
await new Promise(resolve => {
|
|
ctx.TpdsUpdateHandler.promises.newUpdate.rejects(
|
|
new Errors.TooManyRequestsError('project on cooldown')
|
|
)
|
|
const res = {
|
|
sendStatus: status => {
|
|
expect(status).to.equal(429)
|
|
resolve()
|
|
},
|
|
}
|
|
ctx.TpdsController.mergeUpdate(ctx.req, res)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('getting a delete update', function () {
|
|
it('should process the delete with the update receiver by name', async function (ctx) {
|
|
await new Promise(resolve => {
|
|
const path = '/projectName/here.txt'
|
|
const req = {
|
|
params: { 0: path, user_id: ctx.user_id, project_id: '' },
|
|
session: {
|
|
destroy() {},
|
|
},
|
|
headers: {
|
|
'x-update-source': (ctx.source = 'dropbox'),
|
|
},
|
|
}
|
|
const res = {
|
|
sendStatus: () => {
|
|
ctx.TpdsUpdateHandler.promises.deleteUpdate
|
|
.calledWith(
|
|
ctx.user_id,
|
|
'',
|
|
'projectName',
|
|
'/here.txt',
|
|
ctx.source
|
|
)
|
|
.should.equal(true)
|
|
resolve()
|
|
},
|
|
}
|
|
ctx.TpdsController.deleteUpdate(req, res)
|
|
})
|
|
})
|
|
|
|
it('should process the delete with the update receiver by id', async function (ctx) {
|
|
await new Promise(resolve => {
|
|
const path = '/here.txt'
|
|
const req = {
|
|
params: { 0: path, user_id: ctx.user_id, project_id: '123' },
|
|
session: {
|
|
destroy() {},
|
|
},
|
|
headers: {
|
|
'x-update-source': (ctx.source = 'dropbox'),
|
|
},
|
|
}
|
|
const res = {
|
|
sendStatus: () => {
|
|
ctx.TpdsUpdateHandler.promises.deleteUpdate.should.have.been.calledWith(
|
|
ctx.user_id,
|
|
'123',
|
|
'', // projectName
|
|
'/here.txt',
|
|
ctx.source
|
|
)
|
|
resolve()
|
|
},
|
|
}
|
|
ctx.TpdsController.deleteUpdate(req, res)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('updateFolder', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.req = {
|
|
body: { userId: ctx.user_id, path: '/abc/def/ghi.txt' },
|
|
}
|
|
ctx.res = {
|
|
json: sinon.stub(),
|
|
}
|
|
})
|
|
|
|
it("creates a folder if it doesn't exist", async function (ctx) {
|
|
await new Promise(resolve => {
|
|
const metadata = {
|
|
folderId: new ObjectId(),
|
|
projectId: new ObjectId(),
|
|
path: '/def/ghi.txt',
|
|
parentFolderId: new ObjectId(),
|
|
}
|
|
ctx.TpdsUpdateHandler.promises.createFolder.resolves(metadata)
|
|
ctx.res.json.callsFake(body => {
|
|
expect(body).to.deep.equal({
|
|
entityId: metadata.folderId.toString(),
|
|
projectId: metadata.projectId.toString(),
|
|
path: metadata.path,
|
|
folderId: metadata.parentFolderId.toString(),
|
|
})
|
|
resolve()
|
|
})
|
|
ctx.TpdsController.updateFolder(ctx.req, ctx.res)
|
|
})
|
|
})
|
|
|
|
it('supports top level folders', async function (ctx) {
|
|
await new Promise(resolve => {
|
|
const metadata = {
|
|
folderId: new ObjectId(),
|
|
projectId: new ObjectId(),
|
|
path: '/',
|
|
parentFolderId: null,
|
|
}
|
|
ctx.TpdsUpdateHandler.promises.createFolder.resolves(metadata)
|
|
ctx.res.json.callsFake(body => {
|
|
expect(body).to.deep.equal({
|
|
entityId: metadata.folderId.toString(),
|
|
projectId: metadata.projectId.toString(),
|
|
path: metadata.path,
|
|
folderId: null,
|
|
})
|
|
resolve()
|
|
})
|
|
ctx.TpdsController.updateFolder(ctx.req, ctx.res)
|
|
})
|
|
})
|
|
|
|
it("returns a 409 if the folder couldn't be created", async function (ctx) {
|
|
await new Promise(resolve => {
|
|
ctx.TpdsUpdateHandler.promises.createFolder.resolves(null)
|
|
ctx.HttpErrorHandler.conflict.callsFake((req, res) => {
|
|
expect(req).to.equal(ctx.req)
|
|
expect(res).to.equal(ctx.res)
|
|
resolve()
|
|
})
|
|
ctx.TpdsController.updateFolder(ctx.req, ctx.res)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('parseParams', function () {
|
|
it('should take the project name off the start and replace with slash', function (ctx) {
|
|
const path = 'noSlashHere'
|
|
const req = { params: { 0: path, user_id: ctx.user_id } }
|
|
const result = ctx.TpdsController.parseParams(req)
|
|
result.userId.should.equal(ctx.user_id)
|
|
result.filePath.should.equal('/')
|
|
result.projectName.should.equal(path)
|
|
})
|
|
|
|
it('should take the project name off the start and it with no slashes in', function (ctx) {
|
|
const path = '/project/file.tex'
|
|
const req = { params: { 0: path, user_id: ctx.user_id } }
|
|
const result = ctx.TpdsController.parseParams(req)
|
|
result.userId.should.equal(ctx.user_id)
|
|
result.filePath.should.equal('/file.tex')
|
|
result.projectName.should.equal('project')
|
|
})
|
|
|
|
it('should take the project name of and return a slash for the file path', function (ctx) {
|
|
const path = '/project_name'
|
|
const req = { params: { 0: path, user_id: ctx.user_id } }
|
|
const result = ctx.TpdsController.parseParams(req)
|
|
result.projectName.should.equal('project_name')
|
|
result.filePath.should.equal('/')
|
|
})
|
|
})
|
|
|
|
describe('updateProjectContents', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.req = {
|
|
params: {
|
|
0: (ctx.path = 'chapters/main.tex'),
|
|
project_id: (ctx.project_id = 'project-id-123'),
|
|
},
|
|
session: {
|
|
destroy: sinon.stub(),
|
|
},
|
|
headers: {
|
|
'x-update-source': (ctx.source = 'github'),
|
|
},
|
|
}
|
|
|
|
ctx.res = {
|
|
json: sinon.stub(),
|
|
sendStatus: sinon.stub(),
|
|
}
|
|
|
|
await ctx.TpdsController.promises.updateProjectContents(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('should merge the update', function (ctx) {
|
|
ctx.UpdateMerger.promises.mergeUpdate.should.be.calledWith(
|
|
null,
|
|
ctx.project_id,
|
|
`/${ctx.path}`,
|
|
ctx.req,
|
|
ctx.source
|
|
)
|
|
})
|
|
|
|
it('should return a success', function (ctx) {
|
|
ctx.res.json.should.be.calledWith({
|
|
entityId: ctx.metadata.entityId.toString(),
|
|
rev: ctx.metadata.rev,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('deleteProjectContents', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.req = {
|
|
params: {
|
|
0: (ctx.path = 'chapters/main.tex'),
|
|
project_id: (ctx.project_id = 'project-id-123'),
|
|
},
|
|
session: {
|
|
destroy: sinon.stub(),
|
|
},
|
|
headers: {
|
|
'x-update-source': (ctx.source = 'github'),
|
|
},
|
|
}
|
|
ctx.res = {
|
|
sendStatus: sinon.stub(),
|
|
json: sinon.stub(),
|
|
}
|
|
|
|
await ctx.TpdsController.promises.deleteProjectContents(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('should delete the file', function (ctx) {
|
|
ctx.UpdateMerger.promises.deleteUpdate.should.be.calledWith(
|
|
null,
|
|
ctx.project_id,
|
|
`/${ctx.path}`,
|
|
ctx.source
|
|
)
|
|
})
|
|
|
|
it('should return a success', function (ctx) {
|
|
ctx.res.json.should.be.calledWith({
|
|
entityId: ctx.metadata.entityId,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('getQueues', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.req = {}
|
|
ctx.res = { json: sinon.stub() }
|
|
ctx.next = sinon.stub()
|
|
})
|
|
|
|
describe('success', function () {
|
|
beforeEach(async function (ctx) {
|
|
await new Promise(resolve => {
|
|
ctx.res.json.callsFake(() => {
|
|
resolve()
|
|
})
|
|
ctx.TpdsController.getQueues(ctx.req, ctx.res, ctx.next)
|
|
})
|
|
})
|
|
|
|
it('should use userId from session', function (ctx) {
|
|
ctx.SessionManager.getLoggedInUserId.should.have.been.calledOnce
|
|
ctx.TpdsQueueManager.promises.getQueues.should.have.been.calledWith(
|
|
'user-id'
|
|
)
|
|
})
|
|
|
|
it('should call json with response', function (ctx) {
|
|
ctx.res.json.should.have.been.calledWith('queues')
|
|
ctx.next.should.not.have.been.called
|
|
})
|
|
})
|
|
|
|
describe('error', function () {
|
|
beforeEach(async function (ctx) {
|
|
await new Promise(resolve => {
|
|
ctx.err = new Error()
|
|
ctx.TpdsQueueManager.promises.getQueues = sinon
|
|
.stub()
|
|
.rejects(ctx.err)
|
|
ctx.next.callsFake(() => {
|
|
resolve()
|
|
})
|
|
ctx.TpdsController.getQueues(ctx.req, ctx.res, ctx.next)
|
|
})
|
|
})
|
|
|
|
it('should call next with error', function (ctx) {
|
|
ctx.res.json.should.not.have.been.called
|
|
ctx.next.should.have.been.calledWith(ctx.err)
|
|
})
|
|
})
|
|
})
|
|
})
|