| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998 |
- const { expect } = require('chai')
- const sinon = require('sinon')
- const Errors = require('../../../../app/src/Features/Errors/Errors')
- const SandboxedModule = require('sandboxed-module')
- const { ObjectId } = require('mongodb-legacy')
- const MODULE_PATH =
- '../../../../app/src/Features/Project/ProjectEntityUpdateHandler'
- describe('ProjectEntityUpdateHandler', function () {
- const projectId = '4eecb1c1bffa66588e0000a1'
- const projectHistoryId = '123456'
- const docId = '4eecb1c1bffa66588e0000a2'
- const fileId = '4eecaffcbffa66588e000009'
- const folderId = '4eecaffcbffa66588e000008'
- const newFileId = '4eecaffcbffa66588e000099'
- const userId = 1234
- beforeEach(function () {
- this.project = {
- _id: projectId,
- name: 'project name',
- overleaf: {
- history: {
- id: projectHistoryId,
- },
- },
- }
- this.fileUrl = 'filestore.example.com/file'
- this.user = { _id: new ObjectId() }
- this.DocModel = class Doc {
- constructor(options) {
- this.name = options.name
- this.lines = options.lines
- this._id = docId
- this.rev = options.rev ?? 0
- }
- }
- this.FileModel = class File {
- constructor(options) {
- this.name = options.name
- // use a new id for replacement files
- if (this.name === 'dummy-upload-filename') {
- this._id = newFileId
- } else {
- this._id = fileId
- }
- this.rev = 0
- if (options.linkedFileData != null) {
- this.linkedFileData = options.linkedFileData
- }
- if (options.hash != null) {
- this.hash = options.hash
- }
- }
- }
- this.docName = 'doc-name'
- this.docLines = ['1234', 'abc']
- this.doc = { _id: new ObjectId(), name: this.docName }
- this.fileName = 'something.jpg'
- this.fileSystemPath = 'somehintg'
- this.file = { _id: new ObjectId(), name: this.fileName, rev: 2 }
- this.linkedFileData = { provider: 'url' }
- this.source = 'editor'
- this.callback = sinon.stub()
- this.DocstoreManager = {
- promises: {
- getDoc: sinon.stub(),
- isDocDeleted: sinon.stub(),
- updateDoc: sinon.stub(),
- deleteDoc: sinon.stub(),
- },
- }
- this.DocumentUpdaterHandler = {
- promises: {
- flushDocToMongo: sinon.stub().resolves(),
- flushProjectToMongo: sinon.stub().resolves(),
- updateProjectStructure: sinon.stub().resolves(),
- setDocument: sinon.stub(),
- resyncProjectHistory: sinon.stub().resolves(),
- deleteDoc: sinon.stub().resolves(),
- },
- }
- this.fs = {
- promises: {
- unlink: sinon.stub().resolves(),
- },
- }
- this.LockManager = {
- promises: {
- runWithLock: sinon.spy((namespace, id, runner, callback) =>
- runner(callback)
- ),
- },
- withTimeout: sinon.stub().returns(this.LockManager),
- }
- this.ProjectModel = {
- updateOne: sinon.stub(),
- }
- this.ProjectGetter = {
- promises: {
- getProject: sinon.stub(),
- getProjectWithoutDocLines: sinon.stub(),
- },
- }
- this.ProjectLocator = {
- promises: {
- findElement: sinon.stub(),
- findElementByPath: sinon.stub(),
- },
- }
- this.ProjectUpdater = {
- promises: {
- markAsUpdated: sinon.stub().resolves(),
- },
- }
- this.ProjectEntityHandler = {
- getAllEntitiesFromProject: sinon.stub(),
- promises: {
- getDoc: sinon.stub(),
- getDocPathByProjectIdAndDocId: sinon.stub(),
- },
- }
- this.ProjectEntityMongoUpdateHandler = {
- promises: {
- addDoc: sinon.stub(),
- addFile: sinon.stub(),
- addFolder: sinon.stub(),
- _confirmFolder: sinon.stub(),
- _putElement: sinon.stub(),
- _insertDeletedFileReference: sinon.stub(),
- replaceFileWithNew: sinon.stub(),
- mkdirp: sinon.stub(),
- moveEntity: sinon.stub(),
- renameEntity: sinon.stub().resolves({}),
- deleteEntity: sinon.stub(),
- replaceDocWithFile: sinon.stub(),
- replaceFileWithDoc: sinon.stub(),
- },
- }
- this.TpdsUpdateSender = {
- promises: {
- addFile: sinon.stub().resolves(),
- addDoc: sinon.stub(),
- deleteEntity: sinon.stub().resolves(),
- moveEntity: sinon.stub().resolves(),
- },
- }
- this.FileStoreHandler = {
- promises: {
- copyFile: sinon.stub(),
- uploadFileFromDisk: sinon.stub(),
- deleteFile: sinon.stub(),
- },
- _buildUrl: sinon
- .stub()
- .callsFake(
- (projectId, fileId) => `www.filestore.test/${projectId}/${fileId}`
- ),
- }
- this.FileWriter = {
- promises: {
- writeLinesToDisk: sinon.stub(),
- },
- }
- this.EditorRealTimeController = {
- emitToRoom: sinon.stub(),
- }
- this.ProjectOptionsHandler = {
- setHistoryRangesSupport: sinon.stub().resolves(),
- }
- this.ProjectEntityUpdateHandler = SandboxedModule.require(MODULE_PATH, {
- requires: {
- '@overleaf/settings': { validRootDocExtensions: ['tex'] },
- fs: this.fs,
- '../../models/Doc': { Doc: this.DocModel },
- '../Docstore/DocstoreManager': this.DocstoreManager,
- '../../Features/DocumentUpdater/DocumentUpdaterHandler':
- this.DocumentUpdaterHandler,
- '../../models/File': { File: this.FileModel },
- '../FileStore/FileStoreHandler': this.FileStoreHandler,
- '../../infrastructure/LockManager': this.LockManager,
- '../../models/Project': { Project: this.ProjectModel },
- './ProjectGetter': this.ProjectGetter,
- './ProjectLocator': this.ProjectLocator,
- './ProjectUpdateHandler': this.ProjectUpdater,
- './ProjectEntityHandler': this.ProjectEntityHandler,
- './ProjectEntityMongoUpdateHandler':
- this.ProjectEntityMongoUpdateHandler,
- './ProjectOptionsHandler': this.ProjectOptionsHandler,
- '../ThirdPartyDataStore/TpdsUpdateSender': this.TpdsUpdateSender,
- '../Editor/EditorRealTimeController': this.EditorRealTimeController,
- '../../infrastructure/FileWriter': this.FileWriter,
- },
- })
- })
- describe('updateDocLines', function () {
- beforeEach(function () {
- this.path = '/somewhere/something.tex'
- this.doc = {
- _id: docId,
- }
- this.version = 42
- this.ranges = { mock: 'ranges' }
- this.lastUpdatedAt = new Date().getTime()
- this.lastUpdatedBy = 'fake-last-updater-id'
- this.parentFolder = { _id: new ObjectId() }
- this.DocstoreManager.promises.isDocDeleted.resolves(false)
- this.ProjectGetter.promises.getProject.resolves(this.project)
- this.ProjectLocator.promises.findElement.resolves({
- element: this.doc,
- path: {
- fileSystem: this.path,
- },
- folder: this.parentFolder,
- })
- this.TpdsUpdateSender.promises.addDoc.resolves()
- })
- describe('when the doc has been modified', function () {
- beforeEach(function (done) {
- this.DocstoreManager.promises.updateDoc.resolves({
- modified: true,
- rev: (this.rev = 5),
- })
- const callback = (...args) => {
- this.callback(...args)
- done()
- }
- this.ProjectEntityUpdateHandler.updateDocLines(
- projectId,
- docId,
- this.docLines,
- this.version,
- this.ranges,
- this.lastUpdatedAt,
- this.lastUpdatedBy,
- callback
- )
- })
- it('should get the project with very few fields', function () {
- this.ProjectGetter.promises.getProject
- .calledWith(projectId, {
- name: true,
- rootFolder: true,
- })
- .should.equal(true)
- })
- it('should find the doc', function () {
- this.ProjectLocator.promises.findElement
- .calledWith({
- project: this.project,
- type: 'docs',
- element_id: docId,
- })
- .should.equal(true)
- })
- it('should update the doc in the docstore', function () {
- this.DocstoreManager.promises.updateDoc
- .calledWith(
- projectId,
- docId,
- this.docLines,
- this.version,
- this.ranges
- )
- .should.equal(true)
- })
- it('should mark the project as updated', function () {
- sinon.assert.calledWith(
- this.ProjectUpdater.promises.markAsUpdated,
- projectId,
- this.lastUpdatedAt,
- this.lastUpdatedBy
- )
- })
- it('should send the doc the to the TPDS', function () {
- this.TpdsUpdateSender.promises.addDoc.should.have.been.calledWith({
- projectId,
- projectName: this.project.name,
- docId,
- rev: this.rev,
- path: this.path,
- folderId: this.parentFolder._id,
- })
- })
- it('should call the callback', function () {
- this.callback.called.should.equal(true)
- })
- })
- describe('when the doc has not been modified', function () {
- beforeEach(function (done) {
- this.DocstoreManager.promises.updateDoc.resolves({
- modified: false,
- rev: (this.rev = 5),
- })
- const callback = () => {
- this.callback()
- done()
- }
- this.ProjectEntityUpdateHandler.updateDocLines(
- projectId,
- docId,
- this.docLines,
- this.version,
- this.ranges,
- this.lastUpdatedAt,
- this.lastUpdatedBy,
- callback
- )
- })
- it('should not mark the project as updated', function () {
- this.ProjectUpdater.promises.markAsUpdated.called.should.equal(false)
- })
- it('should not send the doc the to the TPDS', function () {
- this.TpdsUpdateSender.promises.addDoc.called.should.equal(false)
- })
- it('should call the callback', function () {
- this.callback.called.should.equal(true)
- })
- })
- describe('when the doc has been deleted', function () {
- beforeEach(function (done) {
- this.ProjectGetter.promises.getProject.resolves(this.project)
- this.ProjectLocator.promises.findElement.rejects(
- new Errors.NotFoundError()
- )
- this.DocstoreManager.promises.isDocDeleted.resolves(true)
- this.DocstoreManager.promises.updateDoc.resolves()
- this.ProjectEntityUpdateHandler.updateDocLines(
- projectId,
- docId,
- this.docLines,
- this.version,
- this.ranges,
- this.lastUpdatedAt,
- this.lastUpdatedBy,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('should update the doc in the docstore', function () {
- this.DocstoreManager.promises.updateDoc
- .calledWith(
- projectId,
- docId,
- this.docLines,
- this.version,
- this.ranges
- )
- .should.equal(true)
- })
- it('should not mark the project as updated', function () {
- this.ProjectUpdater.promises.markAsUpdated.called.should.equal(false)
- })
- it('should not send the doc the to the TPDS', function () {
- this.TpdsUpdateSender.promises.addDoc.called.should.equal(false)
- })
- it('should call the callback', function () {
- this.callback.called.should.equal(true)
- })
- })
- describe('when projects and docs collection are de-synced', function () {
- beforeEach(function (done) {
- this.ProjectGetter.promises.getProject.resolves(this.project)
- // The doc is not in the file-tree, but also not marked as deleted.
- // This should not happen, but web should handle it.
- this.ProjectLocator.promises.findElement.rejects(
- new Errors.NotFoundError()
- )
- this.DocstoreManager.promises.isDocDeleted.resolves(false)
- this.DocstoreManager.promises.updateDoc.resolves()
- const callback = (...args) => {
- this.callback(...args)
- done()
- }
- this.ProjectEntityUpdateHandler.updateDocLines(
- projectId,
- docId,
- this.docLines,
- this.version,
- this.ranges,
- this.lastUpdatedAt,
- this.lastUpdatedBy,
- callback
- )
- })
- it('should update the doc in the docstore', function () {
- this.DocstoreManager.promises.updateDoc
- .calledWith(
- projectId,
- docId,
- this.docLines,
- this.version,
- this.ranges
- )
- .should.equal(true)
- })
- it('should not mark the project as updated', function () {
- this.ProjectUpdater.promises.markAsUpdated.called.should.equal(false)
- })
- it('should not send the doc the to the TPDS', function () {
- this.TpdsUpdateSender.promises.addDoc.called.should.equal(false)
- })
- it('should call the callback', function () {
- this.callback.called.should.equal(true)
- })
- })
- describe('when the doc is not related to the project', function () {
- beforeEach(function (done) {
- this.ProjectGetter.promises.getProject.resolves(this.project)
- this.ProjectLocator.promises.findElement.rejects(
- new Errors.NotFoundError()
- )
- this.DocstoreManager.promises.isDocDeleted.rejects(
- new Errors.NotFoundError()
- )
- const callback = (...args) => {
- this.callback(...args)
- done()
- }
- this.ProjectEntityUpdateHandler.updateDocLines(
- projectId,
- docId,
- this.docLines,
- this.version,
- this.ranges,
- this.lastUpdatedAt,
- this.lastUpdatedBy,
- callback
- )
- })
- it('should return a not found error', function () {
- this.callback
- .calledWith(sinon.match.instanceOf(Errors.NotFoundError))
- .should.equal(true)
- })
- it('should not update the doc', function () {
- this.DocstoreManager.promises.updateDoc.called.should.equal(false)
- })
- it('should not send the doc the to the TPDS', function () {
- this.TpdsUpdateSender.promises.addDoc.called.should.equal(false)
- })
- })
- describe('when the project is not found', function () {
- beforeEach(function (done) {
- this.ProjectGetter.promises.getProject.rejects(
- new Errors.NotFoundError()
- )
- this.ProjectEntityUpdateHandler.updateDocLines(
- projectId,
- docId,
- this.docLines,
- this.version,
- this.ranges,
- this.lastUpdatedAt,
- this.lastUpdatedBy,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('should return a not found error', function () {
- this.callback
- .calledWith(sinon.match.instanceOf(Errors.NotFoundError))
- .should.equal(true)
- })
- it('should not update the doc', function () {
- this.DocstoreManager.promises.updateDoc.called.should.equal(false)
- })
- it('should not send the doc the to the TPDS', function () {
- this.TpdsUpdateSender.promises.addDoc.called.should.equal(false)
- })
- })
- })
- describe('setRootDoc', function () {
- beforeEach(function () {
- this.rootDocId = 'root-doc-id-123123'
- })
- it('should call Project.updateOne when the doc exists and has a valid extension', function (done) {
- this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId.resolves(
- `/main.tex`
- )
- this.ProjectEntityUpdateHandler.setRootDoc(
- projectId,
- this.rootDocId,
- () => {
- this.ProjectModel.updateOne
- .calledWith({ _id: projectId }, { rootDoc_id: this.rootDocId })
- .should.equal(true)
- done()
- }
- )
- })
- it("should not call Project.updateOne when the doc doesn't exist", function (done) {
- this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId.rejects(
- Errors.NotFoundError
- )
- this.ProjectEntityUpdateHandler.setRootDoc(
- projectId,
- this.rootDocId,
- () => {
- this.ProjectModel.updateOne
- .calledWith({ _id: projectId }, { rootDoc_id: this.rootDocId })
- .should.equal(false)
- done()
- }
- )
- })
- it('should call the callback with an UnsupportedFileTypeError when the doc has an unaccepted file extension', function () {
- this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId.resolves(
- `/foo/bar.baz`
- )
- this.ProjectEntityUpdateHandler.setRootDoc(
- projectId,
- this.rootDocId,
- error => {
- expect(error).to.be.an.instanceof(Errors.UnsupportedFileTypeError)
- }
- )
- })
- })
- describe('unsetRootDoc', function () {
- it('should call Project.updateOne', function (done) {
- this.ProjectEntityUpdateHandler.unsetRootDoc(projectId, () => {
- this.ProjectModel.updateOne
- .calledWith({ _id: projectId }, { $unset: { rootDoc_id: true } })
- .should.equal(true)
- done()
- })
- })
- })
- describe('addDoc', function () {
- describe('adding a doc', function () {
- beforeEach(function (done) {
- this.path = '/path/to/doc'
- this.rev = 5
- this.newDoc = new this.DocModel({
- name: this.docName,
- lines: undefined,
- _id: docId,
- rev: this.rev,
- })
- this.DocstoreManager.promises.updateDoc.resolves({
- lines: false,
- rev: this.rev,
- })
- this.TpdsUpdateSender.promises.addDoc.resolves()
- this.ProjectEntityMongoUpdateHandler.promises.addDoc.resolves({
- result: { path: { fileSystem: this.path } },
- project: this.project,
- })
- this.ProjectEntityUpdateHandler.addDoc(
- projectId,
- docId,
- this.docName,
- this.docLines,
- userId,
- this.source,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('creates the doc without history', function () {
- this.DocstoreManager.promises.updateDoc
- .calledWith(projectId, docId, this.docLines, 0, {})
- .should.equal(true)
- })
- it('sends the change in project structure to the doc updater', function () {
- const newDocs = [
- {
- doc: this.newDoc,
- path: this.path,
- docLines: this.docLines.join('\n'),
- ranges: {},
- },
- ]
- this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
- projectId,
- projectHistoryId,
- userId,
- {
- newDocs,
- newProject: this.project,
- },
- this.source
- )
- })
- })
- describe('adding a doc with an invalid name', function () {
- beforeEach(function (done) {
- this.path = '/path/to/doc'
- this.newDoc = { _id: docId }
- this.ProjectEntityUpdateHandler.addDoc(
- projectId,
- folderId,
- `*${this.docName}`,
- this.docLines,
- userId,
- this.source,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('returns an error', function () {
- const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
- this.callback.calledWithMatch(errorMatcher).should.equal(true)
- })
- })
- })
- describe('addFile', function () {
- describe('adding a file', function () {
- beforeEach(function (done) {
- this.path = '/path/to/file'
- this.newFile = {
- _id: fileId,
- rev: 0,
- name: this.fileName,
- linkedFileData: this.linkedFileData,
- }
- this.FileStoreHandler.promises.uploadFileFromDisk.resolves({
- url: this.fileUrl,
- fileRef: this.newFile,
- })
- this.TpdsUpdateSender.promises.addFile.resolves()
- this.ProjectEntityMongoUpdateHandler.promises.addFile.resolves({
- result: { path: { fileSystem: this.path } },
- project: this.project,
- })
- this.ProjectEntityUpdateHandler.addFile(
- projectId,
- folderId,
- this.fileName,
- this.fileSystemPath,
- this.linkedFileData,
- userId,
- this.source,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('updates the file in the filestore', function () {
- this.FileStoreHandler.promises.uploadFileFromDisk
- .calledWith(
- projectId,
- { name: this.fileName, linkedFileData: this.linkedFileData },
- this.fileSystemPath
- )
- .should.equal(true)
- })
- it('updates the file in mongo', function () {
- const fileMatcher = sinon.match(file => {
- return file.name === this.fileName
- })
- this.ProjectEntityMongoUpdateHandler.promises.addFile
- .calledWithMatch(projectId, folderId, fileMatcher)
- .should.equal(true)
- })
- it('notifies the tpds', function () {
- this.TpdsUpdateSender.promises.addFile
- .calledWith({
- projectId,
- projectName: this.project.name,
- fileId,
- rev: 0,
- path: this.path,
- folderId,
- })
- .should.equal(true)
- })
- it('should mark the project as updated', function () {
- const args = this.ProjectUpdater.promises.markAsUpdated.args[0]
- args[0].should.equal(projectId)
- args[1].should.exist
- args[2].should.equal(userId)
- })
- it('sends the change in project structure to the doc updater', function () {
- const newFiles = [
- {
- file: this.newFile,
- path: this.path,
- url: this.fileUrl,
- },
- ]
- this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
- projectId,
- projectHistoryId,
- userId,
- {
- newFiles,
- newProject: this.project,
- },
- this.source
- )
- })
- })
- describe('adding a file with an invalid name', function () {
- beforeEach(function (done) {
- this.path = '/path/to/file'
- this.newFile = {
- _id: fileId,
- rev: 0,
- name: this.fileName,
- linkedFileData: this.linkedFileData,
- }
- this.TpdsUpdateSender.promises.addFile.resolves()
- this.ProjectEntityMongoUpdateHandler.promises.addFile.resolves({
- result: { path: { fileSystem: this.path } },
- project: this.project,
- })
- this.ProjectEntityUpdateHandler.addFile(
- projectId,
- folderId,
- `*${this.fileName}`,
- this.fileSystemPath,
- this.linkedFileData,
- userId,
- this.source,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('returns an error', function () {
- const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
- this.callback.calledWithMatch(errorMatcher).should.equal(true)
- })
- })
- })
- describe('upsertDoc', function () {
- describe('upserting into an invalid folder', function () {
- beforeEach(function (done) {
- this.ProjectLocator.promises.findElement.resolves({ element: null })
- this.ProjectEntityUpdateHandler.upsertDoc(
- projectId,
- folderId,
- this.docName,
- this.docLines,
- this.source,
- userId,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('returns an error', function () {
- const errorMatcher = sinon.match.instanceOf(Error)
- this.callback.calledWithMatch(errorMatcher).should.equal(true)
- })
- })
- describe('updating an existing doc', function () {
- beforeEach(function (done) {
- this.existingDoc = { _id: docId, name: this.docName }
- this.existingFile = { _id: fileId, name: this.fileName }
- this.folder = {
- _id: folderId,
- docs: [this.existingDoc],
- fileRefs: [this.existingFile],
- }
- this.ProjectLocator.promises.findElement.resolves({
- element: this.folder,
- })
- this.DocumentUpdaterHandler.promises.setDocument.resolves()
- this.ProjectEntityUpdateHandler.upsertDoc(
- projectId,
- folderId,
- this.docName,
- this.docLines,
- this.source,
- userId,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('tries to find the folder', function () {
- this.ProjectLocator.promises.findElement
- .calledWith({
- project_id: projectId,
- element_id: folderId,
- type: 'folder',
- })
- .should.equal(true)
- })
- it('updates the doc contents', function () {
- this.DocumentUpdaterHandler.promises.setDocument
- .calledWith(
- projectId,
- this.existingDoc._id,
- userId,
- this.docLines,
- this.source
- )
- .should.equal(true)
- })
- it('returns the doc', function () {
- this.callback.calledWith(null, this.existingDoc, false)
- })
- })
- describe('creating a new doc', function () {
- beforeEach(function (done) {
- this.folder = { _id: folderId, docs: [], fileRefs: [] }
- this.newDoc = { _id: docId }
- this.ProjectLocator.promises.findElement.resolves({
- element: this.folder,
- })
- this.ProjectEntityUpdateHandler.promises.addDocWithRanges = {
- withoutLock: sinon.stub().resolves({ doc: this.newDoc }),
- }
- this.ProjectEntityUpdateHandler.upsertDoc(
- projectId,
- folderId,
- this.docName,
- this.docLines,
- this.source,
- userId,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('tries to find the folder', function () {
- this.ProjectLocator.promises.findElement
- .calledWith({
- project_id: projectId,
- element_id: folderId,
- type: 'folder',
- })
- .should.equal(true)
- })
- it('adds the doc', function () {
- this.ProjectEntityUpdateHandler.promises.addDocWithRanges.withoutLock.should.have.been.calledWith(
- projectId,
- folderId,
- this.docName,
- this.docLines,
- {},
- userId,
- this.source
- )
- })
- it('returns the doc', function () {
- this.callback.calledWith(null, this.newDoc, true)
- })
- })
- describe('upserting a new doc with an invalid name', function () {
- beforeEach(function (done) {
- this.folder = { _id: folderId, docs: [], fileRefs: [] }
- this.newDoc = { _id: docId }
- this.ProjectLocator.promises.findElement.resolves({
- element: this.folder,
- })
- this.ProjectEntityUpdateHandler.promises.addDocWithRanges = {
- withoutLock: sinon.stub().resolves({ doc: this.newDoc }),
- }
- this.ProjectEntityUpdateHandler.upsertDoc(
- projectId,
- folderId,
- `*${this.docName}`,
- this.docLines,
- this.source,
- userId,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('returns an error', function () {
- const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
- this.callback.calledWithMatch(errorMatcher).should.equal(true)
- })
- })
- describe('upserting a doc on top of a file', function () {
- beforeEach(function (done) {
- this.newProject = {
- name: 'new project',
- overleaf: { history: { id: projectHistoryId } },
- }
- this.existingFile = { _id: fileId, name: 'foo.tex', rev: 12 }
- this.folder = { _id: folderId, docs: [], fileRefs: [this.existingFile] }
- this.newDoc = { _id: docId }
- this.docLines = ['line one', 'line two']
- this.folderPath = '/path/to/folder'
- this.filePath = '/path/to/folder/foo.tex'
- this.ProjectLocator.promises.findElement
- .withArgs({
- project_id: projectId,
- element_id: this.folder._id,
- type: 'folder',
- })
- .resolves({
- element: this.folder,
- path: {
- fileSystem: this.folderPath,
- },
- })
- this.DocstoreManager.promises.updateDoc.resolves({ rev: null })
- this.ProjectEntityMongoUpdateHandler.promises.replaceFileWithDoc.resolves(
- this.newProject
- )
- this.TpdsUpdateSender.promises.addDoc.resolves()
- this.ProjectEntityUpdateHandler.upsertDoc(
- projectId,
- folderId,
- 'foo.tex',
- this.docLines,
- this.source,
- userId,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('notifies docstore of the new doc', function () {
- expect(this.DocstoreManager.promises.updateDoc).to.have.been.calledWith(
- projectId,
- this.newDoc._id,
- this.docLines
- )
- })
- it('adds the new doc and removes the file in one go', function () {
- expect(
- this.ProjectEntityMongoUpdateHandler.promises.replaceFileWithDoc
- ).to.have.been.calledWithMatch(
- projectId,
- this.existingFile._id,
- this.newDoc
- )
- })
- it('sends the doc to TPDS', function () {
- expect(this.TpdsUpdateSender.promises.addDoc).to.have.been.calledWith({
- projectId,
- docId: this.newDoc._id,
- path: this.filePath,
- projectName: this.newProject.name,
- rev: this.existingFile.rev + 1,
- folderId,
- })
- })
- it('sends the updates to the doc updater', function () {
- const oldFiles = [
- {
- file: this.existingFile,
- path: this.filePath,
- },
- ]
- const newDocs = [
- {
- doc: sinon.match(this.newDoc),
- path: this.filePath,
- docLines: this.docLines.join('\n'),
- },
- ]
- expect(
- this.DocumentUpdaterHandler.promises.updateProjectStructure
- ).to.have.been.calledWith(
- projectId,
- projectHistoryId,
- userId,
- {
- oldFiles,
- newDocs,
- newProject: this.newProject,
- },
- this.source
- )
- })
- it('should notify everyone of the file deletion', function () {
- expect(
- this.EditorRealTimeController.emitToRoom
- ).to.have.been.calledWith(
- projectId,
- 'removeEntity',
- this.existingFile._id,
- 'convertFileToDoc'
- )
- })
- })
- })
- describe('upsertFile', function () {
- beforeEach(function () {
- this.FileStoreHandler.promises.uploadFileFromDisk.resolves({
- url: this.fileUrl,
- fileRef: this.file,
- })
- })
- describe('upserting into an invalid folder', function () {
- beforeEach(function (done) {
- this.ProjectLocator.promises.findElement.resolves({ element: null })
- this.ProjectEntityUpdateHandler.upsertFile(
- projectId,
- folderId,
- this.fileName,
- this.fileSystemPath,
- this.linkedFileData,
- userId,
- this.source,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('returns an error', function () {
- const errorMatcher = sinon.match.instanceOf(Error)
- this.callback.calledWithMatch(errorMatcher).should.equal(true)
- })
- })
- describe('updating an existing file', function () {
- beforeEach(function (done) {
- this.existingFile = { _id: fileId, name: this.fileName, rev: 1 }
- this.newFile = { _id: new ObjectId(), name: this.fileName, rev: 3 }
- this.folder = { _id: folderId, fileRefs: [this.existingFile], docs: [] }
- this.ProjectLocator.promises.findElement.resolves({
- element: this.folder,
- })
- this.newProject = 'new-project-stub'
- this.ProjectEntityMongoUpdateHandler.promises.replaceFileWithNew.resolves(
- {
- oldFileRef: this.existingFile,
- project: this.project,
- path: { fileSystem: this.fileSystemPath },
- newProject: this.newProject,
- newFileRef: this.newFile,
- }
- )
- this.ProjectEntityUpdateHandler.upsertFile(
- projectId,
- folderId,
- this.fileName,
- this.fileSystemPath,
- this.linkedFileData,
- userId,
- this.source,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('uploads a new version of the file', function () {
- this.FileStoreHandler.promises.uploadFileFromDisk.should.have.been.calledWith(
- projectId,
- {
- name: this.fileName,
- linkedFileData: this.linkedFileData,
- },
- this.fileSystemPath
- )
- })
- it('replaces the file in mongo', function () {
- this.ProjectEntityMongoUpdateHandler.promises.replaceFileWithNew.should.have.been.calledWith(
- projectId,
- this.existingFile._id,
- this.file
- )
- })
- it('notifies the tpds', function () {
- this.TpdsUpdateSender.promises.addFile.should.have.been.calledWith({
- projectId,
- projectName: this.project.name,
- fileId: this.newFile._id,
- rev: this.newFile.rev,
- path: this.fileSystemPath,
- folderId,
- })
- })
- it('should mark the project as updated', function () {
- const args = this.ProjectUpdater.promises.markAsUpdated.args[0]
- args[0].should.equal(projectId)
- args[1].should.exist
- args[2].should.equal(userId)
- })
- it('updates the project structure in the doc updater', function () {
- const oldFiles = [
- {
- file: this.existingFile,
- path: this.fileSystemPath,
- },
- ]
- const newFiles = [
- {
- file: this.newFile,
- path: this.fileSystemPath,
- url: this.fileUrl,
- },
- ]
- this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
- projectId,
- projectHistoryId,
- userId,
- {
- oldFiles,
- newFiles,
- newProject: this.newProject,
- },
- this.source
- )
- })
- it('returns the file', function () {
- this.callback.calledWith(null, this.existingFile, false)
- })
- })
- describe('creating a new file', function () {
- beforeEach(function (done) {
- this.folder = { _id: folderId, fileRefs: [], docs: [] }
- this.newFile = { _id: fileId }
- this.ProjectLocator.promises.findElement.resolves({
- element: this.folder,
- })
- this.FileStoreHandler.promises.uploadFileFromDisk.resolves({
- url: this.fileUrl,
- fileRef: this.newFile,
- })
- this.ProjectEntityUpdateHandler.promises.addFile = {
- mainTask: sinon.stub().resolves(this.newFile),
- }
- this.ProjectEntityUpdateHandler.upsertFile(
- projectId,
- folderId,
- this.fileName,
- this.fileSystemPath,
- this.linkedFileData,
- userId,
- this.source,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('tries to find the folder', function () {
- this.ProjectLocator.promises.findElement.should.have.been.calledWith({
- project_id: projectId,
- element_id: folderId,
- type: 'folder',
- })
- })
- it('adds the file', function () {
- expect(
- this.ProjectEntityUpdateHandler.promises.addFile.mainTask
- ).to.have.been.calledWith({
- projectId,
- folderId,
- userId,
- fileRef: this.newFile,
- fileStoreUrl: this.fileUrl,
- source: this.source,
- })
- })
- it('returns the file', function () {
- this.callback.calledWith(null, this.newFile, true)
- })
- })
- describe('upserting a new file with an invalid name', function () {
- beforeEach(function (done) {
- this.folder = { _id: folderId, fileRefs: [] }
- this.newFile = { _id: fileId }
- this.ProjectLocator.promises.findElement.resolves({
- element: this.folder,
- })
- this.ProjectEntityUpdateHandler.promises.addFile = {
- mainTask: sinon.stub().resolves(this.newFile),
- }
- this.ProjectEntityUpdateHandler.upsertFile(
- projectId,
- folderId,
- `*${this.fileName}`,
- this.fileSystemPath,
- this.linkedFileData,
- userId,
- this.source,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('returns an error', function () {
- const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
- this.callback.calledWithMatch(errorMatcher).should.equal(true)
- })
- })
- describe('upserting file on top of a doc', function () {
- beforeEach(function (done) {
- this.path = '/path/to/doc'
- this.existingDoc = { _id: new ObjectId(), name: this.fileName }
- this.folder = {
- _id: folderId,
- fileRefs: [],
- docs: [this.existingDoc],
- }
- this.ProjectLocator.promises.findElement
- .withArgs({
- project_id: this.project._id.toString(),
- element_id: folderId,
- type: 'folder',
- })
- .resolves({ element: this.folder })
- this.ProjectLocator.promises.findElement
- .withArgs({
- project_id: this.project._id.toString(),
- element_id: this.existingDoc._id,
- type: 'doc',
- })
- .resolves({
- element: this.existingDoc,
- path: { fileSystem: this.path },
- folder: this.folder,
- })
- this.newFileUrl = 'new-file-url'
- this.newFile = {
- _id: newFileId,
- name: 'dummy-upload-filename',
- rev: 0,
- linkedFileData: this.linkedFileData,
- }
- this.newProject = {
- name: 'new project',
- overleaf: { history: { id: projectHistoryId } },
- }
- this.FileStoreHandler.promises.uploadFileFromDisk.resolves({
- url: this.newFileUrl,
- fileRef: this.newFile,
- })
- this.ProjectEntityMongoUpdateHandler.promises.replaceDocWithFile.resolves(
- this.newProject
- )
- this.ProjectEntityUpdateHandler.upsertFile(
- projectId,
- folderId,
- this.fileName,
- this.fileSystemPath,
- this.linkedFileData,
- userId,
- this.source,
- done
- )
- })
- it('replaces the existing doc with a file', function () {
- expect(
- this.ProjectEntityMongoUpdateHandler.promises.replaceDocWithFile
- ).to.have.been.calledWith(projectId, this.existingDoc._id, this.newFile)
- })
- it('updates the doc structure', function () {
- const oldDocs = [
- {
- doc: this.existingDoc,
- path: this.path,
- },
- ]
- const newFiles = [
- {
- file: this.newFile,
- path: this.path,
- url: this.newFileUrl,
- },
- ]
- const updates = {
- oldDocs,
- newFiles,
- newProject: this.newProject,
- }
- expect(
- this.DocumentUpdaterHandler.promises.updateProjectStructure
- ).to.have.been.calledWith(
- projectId,
- projectHistoryId,
- userId,
- updates,
- this.source
- )
- })
- it('tells everyone in the room the doc is removed', function () {
- expect(
- this.EditorRealTimeController.emitToRoom
- ).to.have.been.calledWith(
- projectId,
- 'removeEntity',
- this.existingDoc._id,
- 'convertDocToFile'
- )
- })
- })
- })
- describe('upsertDocWithPath', function () {
- describe('upserting a doc', function () {
- beforeEach(function (done) {
- this.path = '/folder/doc.tex'
- this.newFolders = ['mock-a', 'mock-b']
- this.folder = { _id: folderId }
- this.doc = { _id: docId }
- this.isNewDoc = true
- this.ProjectEntityUpdateHandler.promises.mkdirp = {
- withoutLock: sinon
- .stub()
- .resolves({ newFolders: this.newFolders, folder: this.folder }),
- }
- this.ProjectEntityUpdateHandler.promises.upsertDoc = {
- withoutLock: sinon
- .stub()
- .resolves({ doc: this.doc, isNew: this.isNewDoc }),
- }
- this.ProjectEntityUpdateHandler.upsertDocWithPath(
- projectId,
- this.path,
- this.docLines,
- this.source,
- userId,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('creates any necessary folders', function () {
- this.ProjectEntityUpdateHandler.promises.mkdirp.withoutLock
- .calledWith(projectId, '/folder')
- .should.equal(true)
- })
- it('upserts the doc', function () {
- this.ProjectEntityUpdateHandler.promises.upsertDoc.withoutLock
- .calledWith(
- projectId,
- this.folder._id,
- 'doc.tex',
- this.docLines,
- this.source,
- userId
- )
- .should.equal(true)
- })
- it('calls the callback', function () {
- this.callback
- .calledWith(
- null,
- this.doc,
- this.isNewDoc,
- this.newFolders,
- this.folder
- )
- .should.equal(true)
- })
- })
- describe('upserting a doc with an invalid path', function () {
- beforeEach(function (done) {
- this.path = '/*folder/doc.tex'
- this.newFolders = ['mock-a', 'mock-b']
- this.folder = { _id: folderId }
- this.doc = { _id: docId }
- this.isNewDoc = true
- this.ProjectEntityUpdateHandler.promises.mkdirp = {
- withoutLock: sinon
- .stub()
- .resolves({ newFolders: this.newFolders, folder: this.folder }),
- }
- this.ProjectEntityUpdateHandler.promises.upsertDoc = {
- withoutLock: sinon
- .stub()
- .resolves({ doc: this.doc, isNew: this.isNewDoc }),
- }
- this.ProjectEntityUpdateHandler.upsertDocWithPath(
- projectId,
- this.path,
- this.docLines,
- this.source,
- userId,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('returns an error', function () {
- const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
- this.callback.calledWithMatch(errorMatcher).should.equal(true)
- })
- })
- describe('upserting a doc with an invalid name', function () {
- beforeEach(function (done) {
- this.path = '/folder/*doc.tex'
- this.newFolders = ['mock-a', 'mock-b']
- this.folder = { _id: folderId }
- this.doc = { _id: docId }
- this.isNewDoc = true
- this.ProjectEntityUpdateHandler.promises.mkdirp = {
- withoutLock: sinon
- .stub()
- .resolves({ newFolders: this.newFolders, folder: this.folder }),
- }
- this.ProjectEntityUpdateHandler.promises.upsertDoc = {
- withoutLock: sinon
- .stub()
- .resolves({ doc: this.doc, isNew: this.isNewDoc }),
- }
- this.ProjectEntityUpdateHandler.upsertDocWithPath(
- projectId,
- this.path,
- this.docLines,
- this.source,
- userId,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('returns an error', function () {
- const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
- this.callback.calledWithMatch(errorMatcher).should.equal(true)
- })
- })
- })
- describe('upsertFileWithPath', function () {
- describe('upserting a file', function () {
- beforeEach(function (done) {
- this.path = '/folder/file.png'
- this.newFolders = ['mock-a', 'mock-b']
- this.folder = { _id: folderId }
- this.file = { _id: fileId }
- this.isNewFile = true
- this.FileStoreHandler.promises.uploadFileFromDisk.resolves({
- url: this.fileUrl,
- fileRef: this.newFile,
- })
- this.ProjectEntityUpdateHandler.promises.mkdirp = {
- withoutLock: sinon
- .stub()
- .resolves({ newFolders: this.newFolders, folder: this.folder }),
- }
- this.ProjectEntityUpdateHandler.promises.upsertFile = {
- mainTask: sinon
- .stub()
- .resolves({ fileRef: this.file, isNew: this.isNewFile }),
- }
- this.ProjectEntityUpdateHandler.upsertFileWithPath(
- projectId,
- this.path,
- this.fileSystemPath,
- this.linkedFileData,
- userId,
- this.source,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('creates any necessary folders', function () {
- this.ProjectEntityUpdateHandler.promises.mkdirp.withoutLock
- .calledWith(projectId, '/folder')
- .should.equal(true)
- })
- it('upserts the file', function () {
- this.ProjectEntityUpdateHandler.promises.upsertFile.mainTask.should.have.been.calledWith(
- {
- projectId,
- folderId: this.folder._id,
- fileName: 'file.png',
- fsPath: this.fileSystemPath,
- linkedFileData: this.linkedFileData,
- userId,
- fileRef: this.newFile,
- fileStoreUrl: this.fileUrl,
- source: this.source,
- }
- )
- })
- it('calls the callback', function () {
- this.callback.should.have.been.calledWith(
- null,
- this.file,
- this.isNewFile,
- undefined,
- this.newFolders,
- this.folder
- )
- })
- })
- describe('upserting a file with an invalid path', function () {
- beforeEach(function (done) {
- this.path = '/*folder/file.png'
- this.newFolders = ['mock-a', 'mock-b']
- this.folder = { _id: folderId }
- this.file = { _id: fileId }
- this.isNewFile = true
- this.ProjectEntityUpdateHandler.promises.mkdirp = {
- withoutLock: sinon
- .stub()
- .resolves({ newFolders: this.newFolders, folder: this.folder }),
- }
- this.ProjectEntityUpdateHandler.promises.upsertFile = {
- mainTask: sinon
- .stub()
- .resolves({ doc: this.file, isNew: this.isNewFile }),
- }
- this.ProjectEntityUpdateHandler.upsertFileWithPath(
- projectId,
- this.path,
- this.fileSystemPath,
- this.linkedFileData,
- userId,
- this.source,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('returns an error', function () {
- const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
- this.callback.calledWithMatch(errorMatcher).should.equal(true)
- })
- })
- describe('upserting a file with an invalid name', function () {
- beforeEach(function (done) {
- this.path = '/folder/*file.png'
- this.newFolders = ['mock-a', 'mock-b']
- this.folder = { _id: folderId }
- this.file = { _id: fileId }
- this.isNewFile = true
- this.ProjectEntityUpdateHandler.promises.mkdirp = {
- withoutLock: sinon
- .stub()
- .resolves({ newFolders: this.newFolders, folder: this.folder }),
- }
- this.ProjectEntityUpdateHandler.promises.upsertFile = {
- mainTask: sinon
- .stub()
- .resolves({ doc: this.file, isNew: this.isNewFile }),
- }
- this.ProjectEntityUpdateHandler.upsertFileWithPath(
- projectId,
- this.path,
- this.fileSystemPath,
- this.linkedFileData,
- userId,
- this.source,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('returns an error', function () {
- const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
- this.callback.calledWithMatch(errorMatcher).should.equal(true)
- })
- })
- })
- describe('deleteEntity', function () {
- beforeEach(function (done) {
- this.path = '/path/to/doc.tex'
- this.doc = { _id: docId }
- this.projectBeforeDeletion = { _id: projectId, name: 'project' }
- this.newProject = 'new-project'
- this.ProjectEntityMongoUpdateHandler.promises.deleteEntity.resolves({
- entity: this.doc,
- path: { fileSystem: this.path },
- projectBeforeDeletion: this.projectBeforeDeletion,
- newProject: this.newProject,
- })
- this.ProjectEntityUpdateHandler._cleanUpEntity = sinon
- .stub()
- .resolves([{ type: 'doc', entity: this.doc, path: this.path }])
- this.ProjectEntityUpdateHandler.deleteEntity(
- projectId,
- docId,
- 'doc',
- userId,
- this.source,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('deletes the entity in mongo', function () {
- this.ProjectEntityMongoUpdateHandler.promises.deleteEntity
- .calledWith(projectId, docId, 'doc')
- .should.equal(true)
- })
- it('cleans up the doc in the docstore', function () {
- this.ProjectEntityUpdateHandler._cleanUpEntity
- .calledWith(
- this.projectBeforeDeletion,
- this.newProject,
- this.doc,
- 'doc',
- this.path,
- userId,
- this.source
- )
- .should.equal(true)
- })
- it('it notifies the tpds', function () {
- this.TpdsUpdateSender.promises.deleteEntity.should.have.been.calledWith({
- projectId,
- path: this.path,
- projectName: this.projectBeforeDeletion.name,
- entityId: docId,
- entityType: 'doc',
- subtreeEntityIds: [this.doc._id],
- })
- })
- it('retuns the entity_id', function () {
- this.callback.calledWith(null, docId).should.equal(true)
- })
- })
- describe('deleteEntityWithPath', function () {
- describe('when the entity exists', function () {
- beforeEach(function (done) {
- this.doc = { _id: docId }
- this.ProjectLocator.promises.findElementByPath.resolves({
- element: this.doc,
- type: 'doc',
- })
- this.ProjectEntityUpdateHandler.promises.deleteEntity = {
- withoutLock: sinon.stub().resolves(),
- }
- this.path = '/path/to/doc.tex'
- this.ProjectEntityUpdateHandler.deleteEntityWithPath(
- projectId,
- this.path,
- userId,
- this.source,
- done
- )
- })
- it('finds the entity', function () {
- this.ProjectLocator.promises.findElementByPath
- .calledWith({
- project_id: projectId,
- path: this.path,
- exactCaseMatch: true,
- })
- .should.equal(true)
- })
- it('deletes the entity', function () {
- this.ProjectEntityUpdateHandler.promises.deleteEntity.withoutLock.should.have.been.calledWith(
- projectId,
- this.doc._id,
- 'doc',
- userId,
- this.source
- )
- })
- })
- describe('when the entity does not exist', function () {
- beforeEach(function (done) {
- this.ProjectLocator.promises.findElementByPath.resolves({
- element: null,
- })
- this.path = '/doc.tex'
- this.ProjectEntityUpdateHandler.deleteEntityWithPath(
- projectId,
- this.path,
- userId,
- this.source,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('returns an error', function () {
- this.callback.should.have.been.calledWith(
- sinon.match.instanceOf(Errors.NotFoundError)
- )
- })
- })
- })
- describe('mkdirp', function () {
- beforeEach(function (done) {
- this.docPath = '/folder/doc.tex'
- this.ProjectEntityMongoUpdateHandler.promises.mkdirp.resolves({})
- this.ProjectEntityUpdateHandler.mkdirp(projectId, this.docPath, done)
- })
- it('calls ProjectEntityMongoUpdateHandler', function () {
- this.ProjectEntityMongoUpdateHandler.promises.mkdirp
- .calledWith(projectId, this.docPath)
- .should.equal(true)
- })
- })
- describe('mkdirpWithExactCase', function () {
- beforeEach(function (done) {
- this.docPath = '/folder/doc.tex'
- this.ProjectEntityMongoUpdateHandler.promises.mkdirp.resolves({})
- this.ProjectEntityUpdateHandler.mkdirpWithExactCase(
- projectId,
- this.docPath,
- done
- )
- })
- it('calls ProjectEntityMongoUpdateHandler', function () {
- this.ProjectEntityMongoUpdateHandler.promises.mkdirp
- .calledWith(projectId, this.docPath, { exactCaseMatch: true })
- .should.equal(true)
- })
- })
- describe('addFolder', function () {
- describe('adding a folder', function () {
- beforeEach(function (done) {
- this.parentFolderId = '123asdf'
- this.folderName = 'new-folder'
- this.ProjectEntityMongoUpdateHandler.promises.addFolder.resolves({})
- this.ProjectEntityUpdateHandler.addFolder(
- projectId,
- this.parentFolderId,
- this.folderName,
- done
- )
- })
- it('calls ProjectEntityMongoUpdateHandler', function () {
- this.ProjectEntityMongoUpdateHandler.promises.addFolder
- .calledWith(projectId, this.parentFolderId, this.folderName)
- .should.equal(true)
- })
- })
- describe('adding a folder with an invalid name', function () {
- beforeEach(function (done) {
- this.parentFolderId = '123asdf'
- this.folderName = '*new-folder'
- this.ProjectEntityMongoUpdateHandler.promises.addFolder.resolves({})
- this.ProjectEntityUpdateHandler.addFolder(
- projectId,
- this.parentFolderId,
- this.folderName,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('returns an error', function () {
- const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
- this.callback.calledWithMatch(errorMatcher).should.equal(true)
- })
- })
- })
- describe('moveEntity', function () {
- beforeEach(function (done) {
- this.project_name = 'project name'
- this.startPath = '/a.tex'
- this.endPath = '/folder/b.tex'
- this.rev = 2
- this.changes = { newDocs: ['old-doc'], newFiles: ['old-file'] }
- this.ProjectEntityMongoUpdateHandler.promises.moveEntity.resolves({
- project: this.project,
- startPath: this.startPath,
- endPath: this.endPath,
- rev: this.rev,
- changes: this.changes,
- })
- this.ProjectEntityUpdateHandler.moveEntity(
- projectId,
- docId,
- folderId,
- 'doc',
- userId,
- this.source,
- done
- )
- })
- it('moves the entity in mongo', function () {
- this.ProjectEntityMongoUpdateHandler.promises.moveEntity
- .calledWith(projectId, docId, folderId, 'doc')
- .should.equal(true)
- })
- it('notifies tpds', function () {
- this.TpdsUpdateSender.promises.moveEntity
- .calledWith({
- projectId,
- projectName: this.project_name,
- startPath: this.startPath,
- endPath: this.endPath,
- rev: this.rev,
- entityId: docId,
- entityType: 'doc',
- folderId,
- })
- .should.equal(true)
- })
- it('sends the changes in project structure to the doc updater', function () {
- this.DocumentUpdaterHandler.promises.updateProjectStructure
- .calledWith(
- projectId,
- projectHistoryId,
- userId,
- this.changes,
- this.source
- )
- .should.equal(true)
- })
- })
- describe('renameEntity', function () {
- describe('renaming an entity', function () {
- beforeEach(function (done) {
- this.project_name = 'project name'
- this.startPath = '/folder/a.tex'
- this.endPath = '/folder/b.tex'
- this.rev = 2
- this.changes = { newDocs: ['old-doc'], newFiles: ['old-file'] }
- this.newDocName = 'b.tex'
- this.ProjectEntityMongoUpdateHandler.promises.renameEntity.resolves({
- project: this.project,
- startPath: this.startPath,
- endPath: this.endPath,
- rev: this.rev,
- changes: this.changes,
- })
- this.ProjectEntityUpdateHandler.renameEntity(
- projectId,
- docId,
- 'doc',
- this.newDocName,
- userId,
- this.source,
- done
- )
- })
- it('moves the entity in mongo', function () {
- this.ProjectEntityMongoUpdateHandler.promises.renameEntity
- .calledWith(projectId, docId, 'doc', this.newDocName)
- .should.equal(true)
- })
- it('notifies tpds', function () {
- this.TpdsUpdateSender.promises.moveEntity
- .calledWith({
- projectId,
- projectName: this.project_name,
- startPath: this.startPath,
- endPath: this.endPath,
- rev: this.rev,
- entityId: docId,
- entityType: 'doc',
- folderId: null,
- })
- .should.equal(true)
- })
- it('flushes the project in doc updater', function () {
- this.DocumentUpdaterHandler.promises.flushProjectToMongo.should.have.been.calledWith(
- projectId
- )
- })
- it('sends the changes in project structure to the doc updater', function () {
- this.DocumentUpdaterHandler.promises.updateProjectStructure
- .calledWith(
- projectId,
- projectHistoryId,
- userId,
- this.changes,
- this.source
- )
- .should.equal(true)
- })
- })
- describe('renaming an entity to an invalid name', function () {
- beforeEach(function (done) {
- this.project_name = 'project name'
- this.startPath = '/folder/a.tex'
- this.endPath = '/folder/b.tex'
- this.rev = 2
- this.changes = { newDocs: ['old-doc'], newFiles: ['old-file'] }
- this.newDocName = '*b.tex'
- this.ProjectEntityMongoUpdateHandler.promises.renameEntity.resolves({
- project: this.project,
- startPath: this.startPath,
- endPath: this.endPath,
- rev: this.rev,
- changes: this.changes,
- })
- this.ProjectEntityUpdateHandler.renameEntity(
- projectId,
- docId,
- 'doc',
- this.newDocName,
- userId,
- this.source,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('returns an error', function () {
- const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
- this.callback.calledWithMatch(errorMatcher).should.equal(true)
- })
- })
- describe('renaming an entity with a non-string value', function () {
- beforeEach(function (done) {
- this.project_name = 'project name'
- this.startPath = '/folder/a.tex'
- this.endPath = '/folder/b.tex'
- this.rev = 2
- this.changes = { newDocs: ['old-doc'], newFiles: ['old-file'] }
- this.newDocName = ['hello']
- this.ProjectEntityMongoUpdateHandler.promises.renameEntity.resolves({
- project: this.project,
- startPath: this.startPath,
- endPath: this.endPath,
- rev: this.rev,
- changes: this.changes,
- })
- this.ProjectEntityUpdateHandler.renameEntity(
- projectId,
- docId,
- 'doc',
- this.newDocName,
- userId,
- this.source,
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('returns an error', function () {
- const errorMatcher = sinon.match.instanceOf(Error)
- this.callback.calledWithMatch(errorMatcher).should.equal(true)
- expect(
- this.ProjectEntityMongoUpdateHandler.promises.renameEntity.called
- ).to.equal(false)
- })
- })
- })
- describe('resyncProjectHistory', function () {
- describe('a deleted project', function () {
- beforeEach(function (done) {
- this.ProjectGetter.promises.getProject.resolves({})
- this.ProjectEntityUpdateHandler.resyncProjectHistory(
- projectId,
- {},
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('should return an error', function () {
- expect(this.callback).to.have.been.calledWith(
- sinon.match
- .instanceOf(Errors.ProjectHistoryDisabledError)
- .and(
- sinon.match.has(
- 'message',
- `project history not enabled for ${projectId}`
- )
- )
- )
- })
- })
- describe('a project without project-history enabled', function () {
- beforeEach(function (done) {
- this.project.overleaf = {}
- this.ProjectGetter.promises.getProject.resolves(this.project)
- this.ProjectEntityUpdateHandler.resyncProjectHistory(
- projectId,
- {},
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('should return an error', function () {
- expect(this.callback).to.have.been.calledWith(
- sinon.match
- .instanceOf(Errors.ProjectHistoryDisabledError)
- .and(
- sinon.match.has(
- 'message',
- `project history not enabled for ${projectId}`
- )
- )
- )
- })
- })
- describe('a project with project-history enabled', function () {
- const docs = [{ doc: { _id: docId, name: 'main.tex' }, path: 'main.tex' }]
- const files = [
- {
- file: { _id: fileId, name: 'universe.png', hash: '123456' },
- path: 'universe.png',
- },
- ]
- beforeEach(function (done) {
- this.ProjectGetter.promises.getProject.resolves(this.project)
- const folders = []
- this.ProjectEntityHandler.getAllEntitiesFromProject.returns({
- docs,
- files,
- folders,
- })
- this.ProjectEntityUpdateHandler.resyncProjectHistory(
- projectId,
- {},
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('gets the project', function () {
- this.ProjectGetter.promises.getProject.should.have.been.calledWith(
- projectId
- )
- })
- it('gets the entities for the project', function () {
- this.ProjectEntityHandler.getAllEntitiesFromProject.should.have.been.calledWith(
- this.project
- )
- })
- it('uses an extended timeout', function () {
- this.LockManager.withTimeout.should.have.been.calledWith(6 * 60)
- })
- it('tells the doc updater to sync the project', function () {
- this.DocumentUpdaterHandler.promises.resyncProjectHistory
- .calledWith(projectId, projectHistoryId, docs, files)
- .should.equal(true)
- })
- it('calls the callback', function () {
- this.callback.called.should.equal(true)
- })
- })
- describe('a project with duplicate filenames', function () {
- beforeEach(function (done) {
- this.ProjectGetter.promises.getProject.resolves(this.project)
- this.docs = [
- { doc: { _id: 'doc1', name: 'main.tex' }, path: 'main.tex' },
- {
- doc: { _id: 'doc2', name: 'duplicate.tex' },
- path: 'a/b/c/duplicate.tex',
- },
- {
- doc: { _id: 'doc3', name: 'duplicate.tex' },
- path: 'a/b/c/duplicate.tex',
- },
- {
- doc: { _id: 'doc4', name: 'another dupe (22)' },
- path: 'another dupe (22)',
- },
- {
- doc: { _id: 'doc5', name: 'duplicate.tex' },
- path: 'a/b/c/duplicate.tex',
- },
- ]
- this.files = [
- {
- file: { _id: 'file1', name: 'image.jpg', hash: 'hash1' },
- path: 'image.jpg',
- },
- {
- file: { _id: 'file2', name: 'duplicate.jpg', hash: 'hash2' },
- path: 'duplicate.jpg',
- },
- {
- file: { _id: 'file3', name: 'duplicate.jpg', hash: 'hash3' },
- path: 'duplicate.jpg',
- },
- {
- file: { _id: 'file4', name: 'another dupe (22)', hash: 'hash4' },
- path: 'another dupe (22)',
- },
- ]
- this.ProjectEntityHandler.getAllEntitiesFromProject.returns({
- docs: this.docs,
- files: this.files,
- folders: [],
- })
- this.ProjectEntityUpdateHandler.resyncProjectHistory(
- projectId,
- {},
- done
- )
- })
- it('renames the duplicate files', function () {
- const renameEntity =
- this.ProjectEntityMongoUpdateHandler.promises.renameEntity
- expect(renameEntity).to.have.callCount(4)
- expect(renameEntity).to.have.been.calledWith(
- projectId,
- 'doc3',
- 'doc',
- 'duplicate.tex (1)'
- )
- expect(renameEntity).to.have.been.calledWith(
- projectId,
- 'doc5',
- 'doc',
- 'duplicate.tex (2)'
- )
- expect(renameEntity).to.have.been.calledWith(
- projectId,
- 'file3',
- 'file',
- 'duplicate.jpg (1)'
- )
- expect(renameEntity).to.have.been.calledWith(
- projectId,
- 'file4',
- 'file',
- 'another dupe (23)'
- )
- })
- it('tells the doc updater to resync the project', function () {
- const docs = this.docs.map(d => {
- if (d.doc._id === 'doc3') {
- return Object.assign({}, d, { path: 'a/b/c/duplicate.tex (1)' })
- }
- if (d.doc._id === 'doc5') {
- return Object.assign({}, d, { path: 'a/b/c/duplicate.tex (2)' })
- }
- return d
- })
- const files = this.files.map(f => {
- if (f.file._id === 'file3') {
- return Object.assign({}, f, { path: 'duplicate.jpg (1)' })
- }
- if (f.file._id === 'file4') {
- return Object.assign({}, f, { path: 'another dupe (23)' })
- }
- return f
- })
- expect(
- this.DocumentUpdaterHandler.promises.resyncProjectHistory
- ).to.have.been.calledWith(projectId, projectHistoryId, docs, files)
- })
- })
- describe('a project with bad filenames', function () {
- beforeEach(function (done) {
- this.ProjectGetter.promises.getProject.resolves(this.project)
- this.docs = [
- {
- doc: { _id: 'doc1', name: '/d/e/f/test.tex' },
- path: 'a/b/c/d/e/f/test.tex',
- },
- {
- doc: { _id: 'doc2', name: '' },
- path: 'a',
- },
- ]
- this.files = [
- {
- file: { _id: 'file1', name: 'A*.png', hash: 'hash1' },
- path: 'A*.png',
- },
- {
- file: { _id: 'file2', name: 'A_.png', hash: 'hash2' },
- path: 'A_.png',
- },
- ]
- this.ProjectEntityHandler.getAllEntitiesFromProject.returns({
- docs: this.docs,
- files: this.files,
- folders: [],
- })
- this.ProjectEntityUpdateHandler.resyncProjectHistory(
- projectId,
- {},
- done
- )
- })
- it('renames the files', function () {
- const renameEntity =
- this.ProjectEntityMongoUpdateHandler.promises.renameEntity
- expect(renameEntity).to.have.callCount(4)
- expect(renameEntity).to.have.been.calledWith(
- projectId,
- 'doc1',
- 'doc',
- '_d_e_f_test.tex'
- )
- expect(renameEntity).to.have.been.calledWith(
- projectId,
- 'doc2',
- 'doc',
- 'untitled'
- )
- expect(renameEntity).to.have.been.calledWith(
- projectId,
- 'file1',
- 'file',
- 'A_.png'
- )
- expect(renameEntity).to.have.been.calledWith(
- projectId,
- 'file2',
- 'file',
- 'A_.png (1)'
- )
- })
- it('tells the doc updater to resync the project', function () {
- const docs = this.docs.map(d => {
- if (d.doc._id === 'doc1') {
- return Object.assign({}, d, { path: 'a/b/c/_d_e_f_test.tex' })
- }
- if (d.doc._id === 'doc2') {
- return Object.assign({}, d, { path: 'a/untitled' })
- }
- return d
- })
- const files = this.files.map(f => {
- if (f.file._id === 'file1') {
- return Object.assign({}, f, { path: 'A_.png' })
- }
- if (f.file._id === 'file2') {
- return Object.assign({}, f, { path: 'A_.png (1)' })
- }
- return f
- })
- expect(
- this.DocumentUpdaterHandler.promises.resyncProjectHistory
- ).to.have.been.calledWith(projectId, projectHistoryId, docs, files)
- })
- })
- describe('a project with a bad folder name', function () {
- const folders = [
- {
- folder: { _id: 'folder1', name: 'good' },
- path: 'good',
- },
- {
- folder: { _id: 'folder2', name: 'bad*' },
- path: 'bad*',
- },
- ]
- const docs = [
- {
- doc: { _id: 'doc1', name: 'doc1.tex' },
- path: 'good/doc1.tex',
- },
- {
- doc: { _id: 'doc2', name: 'duplicate.tex' },
- path: 'bad*/doc2.tex',
- },
- ]
- const files = []
- beforeEach(function (done) {
- this.ProjectGetter.promises.getProject.resolves(this.project)
- this.ProjectEntityHandler.getAllEntitiesFromProject.returns({
- docs,
- files,
- folders,
- })
- this.ProjectEntityUpdateHandler.resyncProjectHistory(
- projectId,
- {},
- done
- )
- })
- it('renames the folder', function () {
- const renameEntity =
- this.ProjectEntityMongoUpdateHandler.promises.renameEntity
- expect(renameEntity).to.have.callCount(1)
- expect(renameEntity).to.have.been.calledWith(
- projectId,
- 'folder2',
- 'folder',
- 'bad_'
- )
- })
- it('tells the doc updater to resync the project', function () {
- const fixedDocs = docs.map(d => {
- if (d.doc._id === 'doc2') {
- return Object.assign({}, d, { path: 'bad_/doc2.tex' })
- }
- return d
- })
- expect(
- this.DocumentUpdaterHandler.promises.resyncProjectHistory
- ).to.have.been.calledWith(projectId, projectHistoryId, fixedDocs, files)
- })
- })
- describe('a project with duplicate names between a folder and a doc', function () {
- const folders = [
- {
- folder: { _id: 'folder1', name: 'chapters' },
- path: 'chapters',
- },
- ]
- const docs = [
- {
- doc: { _id: 'doc1', name: 'chapters' },
- path: 'chapters',
- },
- {
- doc: { _id: 'doc2', name: 'chapter1.tex' },
- path: 'chapters/chapter1.tex',
- },
- ]
- const files = []
- beforeEach(function (done) {
- this.ProjectGetter.promises.getProject.resolves(this.project)
- this.ProjectEntityHandler.getAllEntitiesFromProject.returns({
- docs,
- files,
- folders,
- })
- this.ProjectEntityUpdateHandler.resyncProjectHistory(
- projectId,
- {},
- done
- )
- })
- it('renames the doc', function () {
- const renameEntity =
- this.ProjectEntityMongoUpdateHandler.promises.renameEntity
- expect(renameEntity).to.have.callCount(1)
- expect(renameEntity).to.have.been.calledWith(
- projectId,
- 'doc1',
- 'doc',
- 'chapters (1)'
- )
- })
- it('tells the doc updater to resync the project', function () {
- const fixedDocs = docs.map(d => {
- if (d.doc._id === 'doc1') {
- return Object.assign({}, d, { path: 'chapters (1)' })
- }
- return d
- })
- expect(
- this.DocumentUpdaterHandler.promises.resyncProjectHistory
- ).to.have.been.calledWith(projectId, projectHistoryId, fixedDocs, files)
- })
- })
- describe('a project with an invalid file tree', function () {
- beforeEach(function (done) {
- this.callback = sinon.stub()
- this.ProjectGetter.promises.getProject.resolves(this.project)
- this.ProjectEntityHandler.getAllEntitiesFromProject.throws()
- this.ProjectEntityUpdateHandler.resyncProjectHistory(
- projectId,
- {},
- (...args) => {
- this.callback(...args)
- done()
- }
- )
- })
- it('calls the callback with an error', function () {
- expect(this.callback).to.have.been.calledWith(
- sinon.match.instanceOf(Error)
- )
- })
- })
- })
- describe('_cleanUpEntity', function () {
- beforeEach(function () {
- this.entityId = '4eecaffcbffa66588e000009'
- this.FileStoreHandler.promises.deleteFile.resolves()
- this.ProjectEntityUpdateHandler.promises.unsetRootDoc = sinon
- .stub()
- .resolves()
- this.ProjectEntityMongoUpdateHandler.promises._insertDeletedFileReference.resolves()
- })
- describe('a file', function () {
- beforeEach(async function () {
- this.path = '/file/system/path.png'
- this.entity = { _id: this.entityId }
- this.newProject = 'new-project'
- this.subtreeListing =
- await this.ProjectEntityUpdateHandler._cleanUpEntity(
- this.project,
- this.newProject,
- this.entity,
- 'file',
- this.path,
- userId,
- this.source
- )
- })
- it('should insert the file into the deletedFiles collection', function () {
- this.ProjectEntityMongoUpdateHandler.promises._insertDeletedFileReference
- .calledWith(this.project._id, this.entity)
- .should.equal(true)
- })
- it('should not delete the file from FileStoreHandler', function () {
- this.FileStoreHandler.promises.deleteFile
- .calledWith(projectId, this.entityId)
- .should.equal(false)
- })
- it('should not attempt to delete from the document updater', function () {
- this.DocumentUpdaterHandler.promises.deleteDoc.called.should.equal(
- false
- )
- })
- it('should send the update to the doc updater', function () {
- const oldFiles = [{ file: this.entity, path: this.path }]
- this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
- projectId,
- projectHistoryId,
- userId,
- {
- oldFiles,
- oldDocs: [],
- newProject: this.newProject,
- },
- this.source
- )
- })
- it('should return a subtree listing containing only the file', function () {
- expect(this.subtreeListing).to.deep.equal([
- { type: 'file', entity: this.entity, path: this.path },
- ])
- })
- })
- describe('a doc', function () {
- beforeEach(async function () {
- this.path = '/file/system/path.tex'
- this.ProjectEntityUpdateHandler._cleanUpDoc = sinon.stub().resolves()
- this.entity = { _id: this.entityId }
- this.newProject = 'new-project'
- this.subtreeListing =
- await this.ProjectEntityUpdateHandler._cleanUpEntity(
- this.project,
- this.newProject,
- this.entity,
- 'doc',
- this.path,
- userId,
- this.source
- )
- })
- it('should clean up the doc', function () {
- this.ProjectEntityUpdateHandler._cleanUpDoc
- .calledWith(this.project, this.entity, this.path, userId)
- .should.equal(true)
- })
- it('should send the update to the doc updater', function () {
- const oldDocs = [{ doc: this.entity, path: this.path }]
- this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
- projectId,
- projectHistoryId,
- userId,
- {
- oldDocs,
- oldFiles: [],
- newProject: this.newProject,
- },
- this.source
- )
- })
- it('should return a subtree listing containing only the doc', function () {
- expect(this.subtreeListing).to.deep.equal([
- { type: 'doc', entity: this.entity, path: this.path },
- ])
- })
- })
- describe('a folder', function () {
- beforeEach(async function () {
- this.folder = {
- folders: [
- {
- name: 'subfolder',
- fileRefs: [
- (this.file1 = { _id: 'file-id-1', name: 'file-name-1' }),
- ],
- docs: [(this.doc1 = { _id: 'doc-id-1', name: 'doc-name-1' })],
- folders: [],
- },
- ],
- fileRefs: [(this.file2 = { _id: 'file-id-2', name: 'file-name-2' })],
- docs: [(this.doc2 = { _id: 'doc-id-2', name: 'doc-name-2' })],
- }
- this.ProjectEntityUpdateHandler._cleanUpDoc = sinon.stub().resolves()
- this.ProjectEntityUpdateHandler._cleanUpFile = sinon.stub().resolves()
- const path = '/folder'
- this.newProject = 'new-project'
- this.subtreeListing =
- await this.ProjectEntityUpdateHandler._cleanUpEntity(
- this.project,
- this.newProject,
- this.folder,
- 'folder',
- path,
- userId,
- this.source
- )
- })
- it('should clean up all sub files', function () {
- this.ProjectEntityUpdateHandler._cleanUpFile.should.have.been.calledWith(
- this.project,
- this.file1
- )
- this.ProjectEntityUpdateHandler._cleanUpFile.should.have.been.calledWith(
- this.project,
- this.file2
- )
- })
- it('should clean up all sub docs', function () {
- this.ProjectEntityUpdateHandler._cleanUpDoc
- .calledWith(
- this.project,
- this.doc1,
- '/folder/subfolder/doc-name-1',
- userId
- )
- .should.equal(true)
- this.ProjectEntityUpdateHandler._cleanUpDoc
- .calledWith(this.project, this.doc2, '/folder/doc-name-2', userId)
- .should.equal(true)
- })
- it('should should send one update to the doc updater for all docs and files', function () {
- const oldFiles = [
- { file: this.file2, path: '/folder/file-name-2' },
- { file: this.file1, path: '/folder/subfolder/file-name-1' },
- ]
- const oldDocs = [
- { doc: this.doc2, path: '/folder/doc-name-2' },
- { doc: this.doc1, path: '/folder/subfolder/doc-name-1' },
- ]
- this.DocumentUpdaterHandler.promises.updateProjectStructure
- .calledWith(
- projectId,
- projectHistoryId,
- userId,
- {
- oldFiles,
- oldDocs,
- newProject: this.newProject,
- },
- this.source
- )
- .should.equal(true)
- })
- it('should return a subtree listing containing all sub-entities', function () {
- expect(this.subtreeListing).to.have.deep.members([
- { type: 'folder', entity: this.folder, path: '/folder' },
- {
- type: 'folder',
- entity: this.folder.folders[0],
- path: '/folder/subfolder',
- },
- {
- type: 'file',
- entity: this.file1,
- path: '/folder/subfolder/file-name-1',
- },
- {
- type: 'doc',
- entity: this.doc1,
- path: '/folder/subfolder/doc-name-1',
- },
- { type: 'file', entity: this.file2, path: '/folder/file-name-2' },
- { type: 'doc', entity: this.doc2, path: '/folder/doc-name-2' },
- ])
- })
- })
- })
- describe('_cleanUpDoc', function () {
- beforeEach(function () {
- this.doc = {
- _id: new ObjectId(),
- name: 'test.tex',
- }
- this.path = '/path/to/doc'
- this.ProjectEntityUpdateHandler.promises.unsetRootDoc = sinon
- .stub()
- .resolves()
- this.DocstoreManager.promises.deleteDoc.resolves()
- })
- describe('when the doc is the root doc', function () {
- beforeEach(async function () {
- this.project.rootDoc_id = this.doc._id
- await this.ProjectEntityUpdateHandler._cleanUpDoc(
- this.project,
- this.doc,
- this.path,
- userId
- )
- })
- it('should unset the root doc', function () {
- this.ProjectEntityUpdateHandler.promises.unsetRootDoc.should.have.been.calledWith(
- projectId
- )
- })
- it('should delete the doc in the doc updater', function () {
- this.DocumentUpdaterHandler.promises.deleteDoc
- .calledWith(projectId, this.doc._id.toString())
- .should.equal(true)
- })
- it('should delete the doc in the doc store', function () {
- this.DocstoreManager.promises.deleteDoc
- .calledWith(projectId, this.doc._id.toString(), 'test.tex')
- .should.equal(true)
- })
- })
- describe('when the doc is not the root doc', function () {
- beforeEach(async function () {
- this.project.rootDoc_id = new ObjectId()
- await this.ProjectEntityUpdateHandler._cleanUpDoc(
- this.project,
- this.doc,
- this.path,
- userId
- )
- })
- it('should not unset the root doc', function () {
- this.ProjectEntityUpdateHandler.promises.unsetRootDoc.called.should.equal(
- false
- )
- })
- })
- })
- describe('convertDocToFile', function () {
- beforeEach(function () {
- this.docPath = '/folder/doc.tex'
- this.docLines = ['line one', 'line two']
- this.tmpFilePath = '/tmp/file'
- this.fileStoreUrl = 'http://filestore/file'
- this.folder = { _id: new ObjectId() }
- this.rev = 3
- this.ProjectLocator.promises.findElement
- .withArgs({
- project_id: this.project._id,
- element_id: this.doc._id,
- type: 'doc',
- })
- .resolves({
- element: this.doc,
- path: { fileSystem: this.path },
- folder: this.folder,
- })
- this.ProjectLocator.promises.findElement
- .withArgs({
- project_id: this.project._id.toString(),
- element_id: this.file._id,
- type: 'file',
- })
- .resolves({
- element: this.file,
- path: this.docPath,
- folder: this.folder,
- })
- this.DocstoreManager.promises.getDoc
- .withArgs(this.project._id, this.doc._id)
- .resolves({ lines: this.docLines, rev: this.rev })
- this.FileWriter.promises.writeLinesToDisk.resolves(this.tmpFilePath)
- this.FileStoreHandler.promises.uploadFileFromDisk.resolves({
- url: this.fileStoreUrl,
- fileRef: this.file,
- })
- this.ProjectEntityMongoUpdateHandler.promises.replaceDocWithFile.resolves(
- this.project
- )
- })
- describe('successfully', function () {
- beforeEach(function (done) {
- this.ProjectEntityUpdateHandler.convertDocToFile(
- this.project._id,
- this.doc._id,
- this.user._id,
- this.source,
- done
- )
- })
- it('deletes the document in doc updater', function () {
- expect(
- this.DocumentUpdaterHandler.promises.deleteDoc
- ).to.have.been.calledWith(this.project._id, this.doc._id)
- })
- it('uploads the file to filestore', function () {
- expect(
- this.FileStoreHandler.promises.uploadFileFromDisk
- ).to.have.been.calledWith(
- this.project._id,
- { name: this.doc.name, rev: this.rev + 1 },
- this.tmpFilePath
- )
- })
- it('cleans up the temporary file', function () {
- expect(this.fs.promises.unlink).to.have.been.calledWith(
- this.tmpFilePath
- )
- })
- it('replaces the doc with the file', function () {
- expect(
- this.ProjectEntityMongoUpdateHandler.promises.replaceDocWithFile
- ).to.have.been.calledWith(this.project._id, this.doc._id, this.file)
- })
- it('notifies document updater of changes', function () {
- expect(
- this.DocumentUpdaterHandler.promises.updateProjectStructure
- ).to.have.been.calledWith(
- this.project._id,
- this.project.overleaf.history.id,
- this.user._id,
- {
- oldDocs: [{ doc: this.doc, path: this.path }],
- newFiles: [
- { file: this.file, path: this.path, url: this.fileStoreUrl },
- ],
- newProject: this.project,
- },
- this.source
- )
- })
- it('should notify real-time of the doc deletion', function () {
- expect(
- this.EditorRealTimeController.emitToRoom
- ).to.have.been.calledWith(
- this.project._id,
- 'removeEntity',
- this.doc._id,
- 'convertDocToFile'
- )
- })
- it('should notify real-time of the file creation', function () {
- expect(
- this.EditorRealTimeController.emitToRoom
- ).to.have.been.calledWith(
- this.project._id,
- 'reciveNewFile',
- this.folder._id,
- this.file,
- 'convertDocToFile',
- null
- )
- })
- })
- describe('when the doc has ranges', function () {
- it('should throw a DocHasRangesError', function (done) {
- this.ranges = { comments: [{ id: 123 }] }
- this.DocstoreManager.promises.getDoc
- .withArgs(this.project._id, this.doc._id)
- .resolves({
- lines: this.docLines,
- rev: 'rev',
- version: 'version',
- ranges: this.ranges,
- })
- this.ProjectEntityUpdateHandler.convertDocToFile(
- this.project._id,
- this.doc._id,
- this.user._id,
- this.source,
- err => {
- expect(err).to.be.instanceof(Errors.DocHasRangesError)
- done()
- }
- )
- })
- })
- })
- })
|