Files
overleaf-cep/services/web/test/unit/src/ThirdPartyDataStore/TpdsController.test.mjs
Andrew Rumble 4cced4dcb8 Rename test files for vitest
GitOrigin-RevId: f8792c0ce5eeb4843a534d3ff83e011d25fb65e0
2025-05-29 08:05:00 +00:00

511 lines
15 KiB
JavaScript

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