ProjectEntityMongoUpdateHandlerTests.js 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. max-len,
  5. no-return-assign,
  6. no-unused-vars,
  7. standard/no-callback-literal,
  8. */
  9. // TODO: This file was created by bulk-decaffeinate.
  10. // Fix any style issues and re-enable lint.
  11. /*
  12. * decaffeinate suggestions:
  13. * DS102: Remove unnecessary code created because of implicit returns
  14. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  15. */
  16. const chai = require('chai')
  17. const { expect } = chai
  18. const { assert } = require('chai')
  19. const should = chai.should()
  20. const sinon = require('sinon')
  21. const tk = require('timekeeper')
  22. const modulePath =
  23. '../../../../app/src/Features/Project/ProjectEntityMongoUpdateHandler'
  24. const Errors = require('../../../../app/src/Features/Errors/Errors')
  25. const { ObjectId } = require('mongoose').Types
  26. const SandboxedModule = require('sandboxed-module')
  27. describe('ProjectEntityMongoUpdateHandler', function() {
  28. const project_id = '4eecb1c1bffa66588e0000a1'
  29. const doc_id = '4eecb1c1bffa66588e0000a2'
  30. const file_id = '4eecb1c1bffa66588e0000a3'
  31. const folder_id = '4eecaffcbffa66588e000008'
  32. beforeEach(function() {
  33. let Folder
  34. this.FolderModel = Folder = class Folder {
  35. constructor(options) {
  36. ;({ name: this.name } = options)
  37. this._id = 'folder_id'
  38. }
  39. }
  40. this.docName = 'doc-name'
  41. this.fileName = 'something.jpg'
  42. this.project = { _id: project_id, name: 'project name' }
  43. this.callback = sinon.stub()
  44. tk.freeze(Date.now())
  45. return (this.subject = SandboxedModule.require(modulePath, {
  46. requires: {
  47. 'logger-sharelatex': (this.logger = {
  48. log: sinon.stub(),
  49. error: sinon.stub(),
  50. err() {}
  51. }),
  52. 'settings-sharelatex': (this.settings = {
  53. maxEntitiesPerProject: 100
  54. }),
  55. '../Cooldown/CooldownManager': (this.CooldownManager = {}),
  56. '../../models/Folder': {
  57. Folder: this.FolderModel
  58. },
  59. '../../infrastructure/LockManager': (this.LockManager = {
  60. runWithLock: sinon.spy((namespace, id, runner, callback) =>
  61. runner(callback)
  62. )
  63. }),
  64. '../../models/Project': {
  65. Project: (this.ProjectModel = {})
  66. },
  67. './ProjectEntityHandler': (this.ProjectEntityHandler = {}),
  68. './ProjectLocator': (this.ProjectLocator = {}),
  69. './ProjectGetter': (this.ProjectGetter = {
  70. getProjectWithoutLock: sinon.stub().yields(null, this.project)
  71. })
  72. }
  73. }))
  74. })
  75. afterEach(() => tk.reset())
  76. describe('addDoc', function() {
  77. beforeEach(function() {
  78. this.subject._confirmFolder = sinon.stub().yields(folder_id)
  79. this.subject._putElement = sinon.stub()
  80. this.doc = { _id: doc_id }
  81. return this.subject.addDoc(project_id, folder_id, this.doc, this.callback)
  82. })
  83. it('gets the project', function() {
  84. return this.ProjectGetter.getProjectWithoutLock
  85. .calledWith(project_id, {
  86. rootFolder: true,
  87. name: true,
  88. overleaf: true
  89. })
  90. .should.equal(true)
  91. })
  92. it('checks the folder exists', function() {
  93. return this.subject._confirmFolder
  94. .calledWith(this.project, folder_id)
  95. .should.equal(true)
  96. })
  97. return it('puts the element in mongo', function() {
  98. return this.subject._putElement
  99. .calledWith(this.project, folder_id, this.doc, 'doc', this.callback)
  100. .should.equal(true)
  101. })
  102. })
  103. describe('addFile', function() {
  104. beforeEach(function() {
  105. this.subject._confirmFolder = sinon.stub().yields(folder_id)
  106. this.subject._putElement = sinon.stub()
  107. this.file = { _id: file_id }
  108. return this.subject.addFile(
  109. project_id,
  110. folder_id,
  111. this.file,
  112. this.callback
  113. )
  114. })
  115. it('gets the project', function() {
  116. return this.ProjectGetter.getProjectWithoutLock
  117. .calledWith(project_id, {
  118. rootFolder: true,
  119. name: true,
  120. overleaf: true
  121. })
  122. .should.equal(true)
  123. })
  124. it('checks the folder exists', function() {
  125. return this.subject._confirmFolder
  126. .calledWith(this.project, folder_id)
  127. .should.equal(true)
  128. })
  129. return it('puts the element in mongo', function() {
  130. return this.subject._putElement
  131. .calledWith(this.project, folder_id, this.file, 'file', this.callback)
  132. .should.equal(true)
  133. })
  134. })
  135. describe('replaceFileWithNew', function() {
  136. beforeEach(function() {
  137. this.file = { _id: file_id }
  138. this.path = { mongo: 'file.png' }
  139. this.newFile = { _id: 'new-file-id' }
  140. this.newFile.linkedFileData = this.linkedFileData = { provider: 'url' }
  141. this.newProject = 'new-project'
  142. this.ProjectLocator.findElement = sinon
  143. .stub()
  144. .yields(null, this.file, this.path)
  145. this.ProjectModel.findOneAndUpdate = sinon
  146. .stub()
  147. .yields(null, this.newProject)
  148. this.ProjectModel.update = sinon.stub().yields()
  149. return this.subject.replaceFileWithNew(
  150. project_id,
  151. file_id,
  152. this.newFile,
  153. this.callback
  154. )
  155. })
  156. it('gets the project', function() {
  157. return this.ProjectGetter.getProjectWithoutLock
  158. .calledWith(project_id, {
  159. rootFolder: true,
  160. name: true,
  161. overleaf: true
  162. })
  163. .should.equal(true)
  164. })
  165. it('finds the existing element', function() {
  166. return this.ProjectLocator.findElement
  167. .calledWith({
  168. project: this.project,
  169. element_id: file_id,
  170. type: 'file'
  171. })
  172. .should.equal(true)
  173. })
  174. it('inserts a deletedFile reference for the old file', function() {
  175. return this.ProjectModel.update
  176. .calledWith(
  177. { _id: project_id },
  178. {
  179. $push: {
  180. deletedFiles: {
  181. _id: file_id,
  182. name: this.file.name,
  183. linkedFileData: this.file.linkedFileData,
  184. hash: this.file.hash,
  185. deletedAt: new Date()
  186. }
  187. }
  188. }
  189. )
  190. .should.equal(true)
  191. })
  192. it('increments the project version and sets the rev and created_at', function() {
  193. return this.ProjectModel.findOneAndUpdate
  194. .calledWith(
  195. { _id: project_id },
  196. {
  197. $inc: { version: 1, 'file.png.rev': 1 },
  198. $set: {
  199. 'file.png._id': this.newFile._id,
  200. 'file.png.created': new Date(),
  201. 'file.png.linkedFileData': this.linkedFileData,
  202. 'file.png.hash': this.hash
  203. }
  204. },
  205. { new: true }
  206. )
  207. .should.equal(true)
  208. })
  209. return it('calls the callback', function() {
  210. return this.callback
  211. .calledWith(null, this.file, this.project, this.path, this.newProject)
  212. .should.equal(true)
  213. })
  214. })
  215. describe('mkdirp', function() {
  216. beforeEach(function() {
  217. this.parentFolder_id = '1jnjknjk'
  218. this.newFolder = { _id: 'newFolder_id_here' }
  219. this.lastFolder = { _id: '123das', folders: [] }
  220. this.rootFolder = { _id: 'rootFolderId' }
  221. this.project = { _id: project_id, rootFolder: [this.rootFolder] }
  222. this.ProjectGetter.getProjectWithOnlyFolders = sinon
  223. .stub()
  224. .yields(null, this.project)
  225. this.ProjectLocator.findElementByPath = function() {}
  226. sinon.stub(this.ProjectLocator, 'findElementByPath', (options, cb) => {
  227. const { path } = options
  228. this.parentFolder = { _id: 'parentFolder_id_here' }
  229. const lastFolder = path.substring(path.lastIndexOf('/'))
  230. if (lastFolder.indexOf('level1') === -1) {
  231. return cb('level1 is not the last foler ')
  232. } else {
  233. return cb(null, this.parentFolder)
  234. }
  235. })
  236. return (this.subject.addFolder = {
  237. withoutLock: (project_id, parentFolder_id, folderName, callback) => {
  238. return callback(null, { name: folderName }, this.parentFolder_id)
  239. }
  240. })
  241. })
  242. it('should return the root folder if the path is just a slash', function(done) {
  243. const path = '/'
  244. return this.subject.mkdirp(
  245. project_id,
  246. path,
  247. {},
  248. (err, folders, lastFolder) => {
  249. lastFolder.should.deep.equal(this.rootFolder)
  250. assert.equal(lastFolder.parentFolder_id, undefined)
  251. return done()
  252. }
  253. )
  254. })
  255. it('should make just one folder', function(done) {
  256. const path = '/differentFolder/'
  257. return this.subject.mkdirp(
  258. project_id,
  259. path,
  260. {},
  261. (err, folders, lastFolder) => {
  262. folders.length.should.equal(1)
  263. lastFolder.name.should.equal('differentFolder')
  264. lastFolder.parentFolder_id.should.equal(this.parentFolder_id)
  265. return done()
  266. }
  267. )
  268. })
  269. it('should make the final folder in path if it doesnt exist with one level', function(done) {
  270. const path = 'level1/level2'
  271. return this.subject.mkdirp(
  272. project_id,
  273. path,
  274. {},
  275. (err, folders, lastFolder) => {
  276. folders.length.should.equal(1)
  277. lastFolder.name.should.equal('level2')
  278. lastFolder.parentFolder_id.should.equal(this.parentFolder_id)
  279. return done()
  280. }
  281. )
  282. })
  283. it('should make the final folder in path if it doesnt exist with mutliple levels', function(done) {
  284. const path = 'level1/level2/level3'
  285. return this.subject.mkdirp(
  286. project_id,
  287. path,
  288. {},
  289. (err, folders, lastFolder) => {
  290. folders.length.should.equal(2)
  291. folders[0].name.should.equal('level2')
  292. folders[0].parentFolder_id.should.equal(this.parentFolder_id)
  293. lastFolder.name.should.equal('level3')
  294. lastFolder.parentFolder_id.should.equal(this.parentFolder_id)
  295. return done()
  296. }
  297. )
  298. })
  299. it('should work with slashes either side', function(done) {
  300. const path = '/level1/level2/level3/'
  301. return this.subject.mkdirp(
  302. project_id,
  303. path,
  304. {},
  305. (err, folders, lastFolder) => {
  306. folders.length.should.equal(2)
  307. folders[0].name.should.equal('level2')
  308. folders[0].parentFolder_id.should.equal(this.parentFolder_id)
  309. lastFolder.name.should.equal('level3')
  310. lastFolder.parentFolder_id.should.equal(this.parentFolder_id)
  311. return done()
  312. }
  313. )
  314. })
  315. it('should use a case-insensitive match by default', function(done) {
  316. const path = '/differentFolder/'
  317. return this.subject.mkdirp(
  318. project_id,
  319. path,
  320. {},
  321. (err, folders, lastFolder) => {
  322. this.ProjectLocator.findElementByPath
  323. .calledWithMatch({ exactCaseMatch: undefined })
  324. .should.equal(true)
  325. return done()
  326. }
  327. )
  328. })
  329. return it('should use a case-sensitive match if exactCaseMatch option is set', function(done) {
  330. const path = '/differentFolder/'
  331. return this.subject.mkdirp(
  332. project_id,
  333. path,
  334. { exactCaseMatch: true },
  335. (err, folders, lastFolder) => {
  336. this.ProjectLocator.findElementByPath
  337. .calledWithMatch({ exactCaseMatch: true })
  338. .should.equal(true)
  339. return done()
  340. }
  341. )
  342. })
  343. })
  344. describe('moveEntity', function() {
  345. beforeEach(function() {
  346. this.pathAfterMove = {
  347. fileSystem: '/somewhere/else.txt'
  348. }
  349. this.ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub()
  350. this.ProjectEntityHandler.getAllEntitiesFromProject
  351. .onFirstCall()
  352. .yields(
  353. null,
  354. (this.oldDocs = ['old-doc']),
  355. (this.oldFiles = ['old-file'])
  356. )
  357. this.ProjectEntityHandler.getAllEntitiesFromProject
  358. .onSecondCall()
  359. .yields(
  360. null,
  361. (this.newDocs = ['new-doc']),
  362. (this.newFiles = ['new-file'])
  363. )
  364. this.doc = { lines: ['1234', '312343d'], rev: '1234' }
  365. this.path = {
  366. mongo: 'folders[0]',
  367. fileSystem: '/old_folder/somewhere.txt'
  368. }
  369. this.newProject = 'new-project'
  370. this.ProjectLocator.findElement = sinon
  371. .stub()
  372. .withArgs({
  373. project: this.project,
  374. element_id: this.docId,
  375. type: 'docs'
  376. })
  377. .yields(null, this.doc, this.path)
  378. this.subject._checkValidMove = sinon.stub().yields()
  379. this.subject._removeElementFromMongoArray = sinon
  380. .stub()
  381. .yields(null, this.newProject)
  382. this.subject._putElement = sinon
  383. .stub()
  384. .yields(null, { path: this.pathAfterMove }, this.newProject)
  385. return this.subject.moveEntity(
  386. project_id,
  387. doc_id,
  388. folder_id,
  389. 'docs',
  390. this.callback
  391. )
  392. })
  393. it('should get the project', function() {
  394. return this.ProjectGetter.getProjectWithoutLock
  395. .calledWith(project_id, {
  396. rootFolder: true,
  397. name: true,
  398. overleaf: true
  399. })
  400. .should.equal(true)
  401. })
  402. it('should find the doc to move', function() {
  403. return this.ProjectLocator.findElement
  404. .calledWith({ element_id: doc_id, type: 'docs', project: this.project })
  405. .should.equal(true)
  406. })
  407. it('should check this is a valid move', function() {
  408. return this.subject._checkValidMove
  409. .calledWith(this.project, 'docs', this.doc, this.path, folder_id)
  410. .should.equal(true)
  411. })
  412. it('should put the element in the new folder', function() {
  413. return this.subject._putElement
  414. .calledWith(this.project, folder_id, this.doc, 'docs')
  415. .should.equal(true)
  416. })
  417. it('should remove the element from its current position', function() {
  418. return this.subject._removeElementFromMongoArray
  419. .calledWith(this.ProjectModel, project_id, this.path.mongo, doc_id)
  420. .should.equal(true)
  421. })
  422. it('should remove the element from its current position after putting the element in the new folder', function() {
  423. return this.subject._removeElementFromMongoArray
  424. .calledAfter(this.subject._putElement)
  425. .should.equal(true)
  426. })
  427. return it('calls the callback', function() {
  428. const changes = {
  429. oldDocs: this.oldDocs,
  430. newDocs: this.newDocs,
  431. oldFiles: this.oldFiles,
  432. newFiles: this.newFiles,
  433. newProject: this.newProject
  434. }
  435. return this.callback
  436. .calledWith(
  437. null,
  438. this.project,
  439. this.path.fileSystem,
  440. this.pathAfterMove.fileSystem,
  441. this.doc.rev,
  442. changes
  443. )
  444. .should.equal(true)
  445. })
  446. })
  447. describe('moveEntity must refuse to move the folder to a subfolder of itself', function() {
  448. beforeEach(function() {
  449. this.pathAfterMove = {
  450. fileSystem: '/somewhere/else.txt'
  451. }
  452. this.doc = { lines: ['1234', '312343d'], rev: '1234' }
  453. this.path = {
  454. mongo: 'folders[0]',
  455. fileSystem: '/old_folder/somewhere.txt'
  456. }
  457. this.newProject = 'new-project'
  458. this.ProjectLocator.findElement = sinon
  459. .stub()
  460. .withArgs({
  461. project: this.project,
  462. element_id: this.docId,
  463. type: 'docs'
  464. })
  465. .yields(null, this.doc, this.path)
  466. // return an error when moving a folder to a subfolder of itself
  467. this.subject._checkValidMove = sinon.stub().yields(new Error())
  468. this.subject._removeElementFromMongoArray = sinon
  469. .stub()
  470. .yields(null, this.project)
  471. this.subject._putElement = sinon
  472. .stub()
  473. .yields(null, { path: this.pathAfterMove }, this.newProject)
  474. return this.subject.moveEntity(
  475. project_id,
  476. doc_id,
  477. folder_id,
  478. 'docs',
  479. this.callback
  480. )
  481. })
  482. it('should get the project', function() {
  483. return this.ProjectGetter.getProjectWithoutLock
  484. .calledWith(project_id, {
  485. rootFolder: true,
  486. name: true,
  487. overleaf: true
  488. })
  489. .should.equal(true)
  490. })
  491. it('should find the doc to move', function() {
  492. return this.ProjectLocator.findElement
  493. .calledWith({ element_id: doc_id, type: 'docs', project: this.project })
  494. .should.equal(true)
  495. })
  496. it('should check this is an invalid move', function() {
  497. return this.subject._checkValidMove
  498. .calledWith(this.project, 'docs', this.doc, this.path, folder_id)
  499. .should.equal(true)
  500. })
  501. it('should not put the element in the new folder', function() {
  502. return this.subject._putElement.called.should.equal(false)
  503. })
  504. it('should not remove the element from its current position', function() {
  505. return this.subject._removeElementFromMongoArray.called.should.equal(
  506. false
  507. )
  508. })
  509. return it('calls the callback with an error', function() {
  510. return this.callback.calledWith(new Error()).should.equal(true)
  511. })
  512. })
  513. describe('deleteEntity', function() {
  514. beforeEach(function() {
  515. this.path = { mongo: 'mongo.path', fileSystem: '/file/system/path' }
  516. this.doc = { _id: doc_id }
  517. this.ProjectLocator.findElement = sinon
  518. .stub()
  519. .callsArgWith(1, null, this.doc, this.path)
  520. this.subject._removeElementFromMongoArray = sinon.stub().yields()
  521. return this.subject.deleteEntity(project_id, doc_id, 'doc', this.callback)
  522. })
  523. it('should get the project', function() {
  524. return this.ProjectGetter.getProjectWithoutLock
  525. .calledWith(project_id, {
  526. rootFolder: true,
  527. name: true,
  528. overleaf: true
  529. })
  530. .should.equal(true)
  531. })
  532. it('should find the element', function() {
  533. return this.ProjectLocator.findElement
  534. .calledWith({
  535. project: this.project,
  536. element_id: this.doc._id,
  537. type: 'doc'
  538. })
  539. .should.equal(true)
  540. })
  541. it('should remove the element from the database', function() {
  542. return this.subject._removeElementFromMongoArray
  543. .calledWith(
  544. this.ProjectModel,
  545. project_id,
  546. this.path.mongo,
  547. this.doc._id
  548. )
  549. .should.equal(true)
  550. })
  551. return it('calls the callbck', function() {
  552. return this.callback
  553. .calledWith(null, this.doc, this.path, this.project)
  554. .should.equal(true)
  555. })
  556. })
  557. describe('renameEntity', function() {
  558. beforeEach(function() {
  559. this.newName = 'new.tex'
  560. this.path = { mongo: 'mongo.path', fileSystem: '/old.tex' }
  561. this.project = {
  562. _id: ObjectId(project_id),
  563. rootFolder: [{ _id: ObjectId() }]
  564. }
  565. this.doc = { _id: doc_id, name: 'old.tex', rev: 1 }
  566. this.folder = { _id: folder_id }
  567. this.newProject = 'new-project'
  568. this.ProjectGetter.getProjectWithoutLock = sinon
  569. .stub()
  570. .yields(null, this.project)
  571. this.ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub()
  572. this.ProjectEntityHandler.getAllEntitiesFromProject
  573. .onFirstCall()
  574. .yields(
  575. null,
  576. (this.oldDocs = ['old-doc']),
  577. (this.oldFiles = ['old-file'])
  578. )
  579. this.ProjectEntityHandler.getAllEntitiesFromProject
  580. .onSecondCall()
  581. .yields(
  582. null,
  583. (this.newDocs = ['new-doc']),
  584. (this.newFiles = ['new-file'])
  585. )
  586. this.ProjectLocator.findElement = sinon
  587. .stub()
  588. .yields(null, this.doc, this.path, this.folder)
  589. this.subject._checkValidElementName = sinon.stub().yields()
  590. this.ProjectModel.findOneAndUpdate = sinon
  591. .stub()
  592. .callsArgWith(3, null, this.newProject)
  593. return this.subject.renameEntity(
  594. project_id,
  595. doc_id,
  596. 'doc',
  597. this.newName,
  598. this.callback
  599. )
  600. })
  601. it('should get the project', function() {
  602. return this.ProjectGetter.getProjectWithoutLock
  603. .calledWith(project_id, {
  604. rootFolder: true,
  605. name: true,
  606. overleaf: true
  607. })
  608. .should.equal(true)
  609. })
  610. it('should find the doc', function() {
  611. return this.ProjectLocator.findElement
  612. .calledWith({ element_id: doc_id, type: 'doc', project: this.project })
  613. .should.equal(true)
  614. })
  615. it('should check the new name is valid', function() {
  616. return this.subject._checkValidElementName
  617. .calledWith(this.folder, this.newName)
  618. .should.equal(true)
  619. })
  620. it('should update the doc name', function() {
  621. return this.ProjectModel.findOneAndUpdate
  622. .calledWith(
  623. { _id: project_id },
  624. { $set: { 'mongo.path.name': this.newName }, $inc: { version: 1 } },
  625. { new: true }
  626. )
  627. .should.equal(true)
  628. })
  629. return it('calls the callback', function() {
  630. const changes = {
  631. oldDocs: this.oldDocs,
  632. newDocs: this.newDocs,
  633. oldFiles: this.oldFiles,
  634. newFiles: this.newFiles,
  635. newProject: this.newProject
  636. }
  637. return this.callback
  638. .calledWith(
  639. null,
  640. this.project,
  641. '/old.tex',
  642. '/new.tex',
  643. this.doc.rev,
  644. changes
  645. )
  646. .should.equal(true)
  647. })
  648. })
  649. describe('addFolder', function() {
  650. beforeEach(function() {
  651. this.folderName = 'folder1234'
  652. this.ProjectGetter.getProjectWithOnlyFolders = sinon
  653. .stub()
  654. .callsArgWith(1, null, this.project)
  655. this.subject._confirmFolder = sinon.stub().yields(folder_id)
  656. this.subject._putElement = sinon.stub().yields()
  657. return this.subject.addFolder(
  658. project_id,
  659. folder_id,
  660. this.folderName,
  661. this.callback
  662. )
  663. })
  664. it('gets the project', function() {
  665. return this.ProjectGetter.getProjectWithoutLock
  666. .calledWith(project_id, {
  667. rootFolder: true,
  668. name: true,
  669. overleaf: true
  670. })
  671. .should.equal(true)
  672. })
  673. it('checks the parent folder exists', function() {
  674. return this.subject._confirmFolder
  675. .calledWith(this.project, folder_id)
  676. .should.equal(true)
  677. })
  678. it('puts the element in mongo', function() {
  679. const folderMatcher = sinon.match(folder => {
  680. return folder.name === this.folderName
  681. })
  682. return this.subject._putElement
  683. .calledWithMatch(this.project, folder_id, folderMatcher, 'folder')
  684. .should.equal(true)
  685. })
  686. return it('calls the callback', function() {
  687. const folderMatcher = sinon.match(folder => {
  688. return folder.name === this.folderName
  689. })
  690. return this.callback
  691. .calledWithMatch(null, folderMatcher, folder_id)
  692. .should.equal(true)
  693. })
  694. })
  695. describe('_removeElementFromMongoArray ', function() {
  696. beforeEach(function() {
  697. this.mongoPath = 'folders[0].folders[5]'
  698. this.id = '12344'
  699. this.entityId = '5678'
  700. this.ProjectModel.update = sinon.stub().yields()
  701. this.ProjectModel.findOneAndUpdate = sinon
  702. .stub()
  703. .yields(null, this.project)
  704. return this.subject._removeElementFromMongoArray(
  705. this.ProjectModel,
  706. this.id,
  707. this.mongoPath,
  708. this.entityId,
  709. this.callback
  710. )
  711. })
  712. it('should pull', function() {
  713. return this.ProjectModel.findOneAndUpdate
  714. .calledWith(
  715. { _id: this.id },
  716. {
  717. $pull: { 'folders[0]': { _id: this.entityId } },
  718. $inc: { version: 1 }
  719. },
  720. { new: true }
  721. )
  722. .should.equal(true)
  723. })
  724. return it('should call the callback', function() {
  725. return this.callback.calledWith(null, this.project).should.equal(true)
  726. })
  727. })
  728. describe('_countElements', function() {
  729. beforeEach(function() {
  730. return (this.project = {
  731. _id: project_id,
  732. rootFolder: [
  733. {
  734. docs: [{ _id: 123 }, { _id: 345 }],
  735. fileRefs: [{ _id: 123 }, { _id: 345 }, { _id: 456 }],
  736. folders: [
  737. {
  738. docs: [{ _id: 123 }, { _id: 345 }, { _id: 456 }],
  739. fileRefs: {},
  740. folders: [
  741. {
  742. docs: [{ _id: 1234 }],
  743. fileRefs: [{ _id: 23123 }, { _id: 123213 }, { _id: 2312 }],
  744. folders: [
  745. {
  746. docs: [{ _id: 321321 }, { _id: 123213 }],
  747. fileRefs: [{ _id: 312321 }],
  748. folders: []
  749. }
  750. ]
  751. }
  752. ]
  753. },
  754. {
  755. docs: [{ _id: 123 }, { _id: 32131 }],
  756. fileRefs: [],
  757. folders: [
  758. {
  759. docs: [{ _id: 3123 }],
  760. fileRefs: [
  761. { _id: 321321 },
  762. { _id: 321321 },
  763. { _id: 313122 }
  764. ],
  765. folders: 0
  766. }
  767. ]
  768. }
  769. ]
  770. }
  771. ]
  772. })
  773. })
  774. it('should return the correct number', function() {
  775. return expect(this.subject._countElements(this.project)).to.equal(26)
  776. })
  777. it('should deal with null folders', function() {
  778. this.project.rootFolder[0].folders[0].folders = undefined
  779. return expect(this.subject._countElements(this.project)).to.equal(17)
  780. })
  781. it('should deal with null docs', function() {
  782. this.project.rootFolder[0].folders[0].docs = undefined
  783. return expect(this.subject._countElements(this.project)).to.equal(23)
  784. })
  785. return it('should deal with null fileRefs', function() {
  786. this.project.rootFolder[0].folders[0].folders[0].fileRefs = undefined
  787. return expect(this.subject._countElements(this.project)).to.equal(23)
  788. })
  789. })
  790. describe('_putElement', function() {
  791. beforeEach(function() {
  792. this.project = {
  793. _id: project_id,
  794. rootFolder: [{ _id: ObjectId() }]
  795. }
  796. this.folder = {
  797. _id: ObjectId(),
  798. name: 'someFolder',
  799. docs: [{ name: 'another-doc.tex' }],
  800. fileRefs: [{ name: 'another-file.tex' }],
  801. folders: [{ name: 'another-folder' }]
  802. }
  803. this.doc = {
  804. _id: ObjectId(),
  805. name: 'new.tex'
  806. }
  807. this.path = { mongo: 'mongo.path', fileSystem: '/file/system/old.tex' }
  808. this.ProjectLocator.findElement = sinon
  809. .stub()
  810. .yields(null, this.folder, this.path)
  811. return (this.ProjectModel.findOneAndUpdate = sinon
  812. .stub()
  813. .yields(null, this.project))
  814. })
  815. return describe('updating the project', function() {
  816. it('should use the correct mongo path', function(done) {
  817. return this.subject._putElement(
  818. this.project,
  819. this.folder._id,
  820. this.doc,
  821. 'docs',
  822. err => {
  823. this.ProjectModel.findOneAndUpdate.args[0][0]._id.should.equal(
  824. this.project._id
  825. )
  826. assert.deepEqual(
  827. this.ProjectModel.findOneAndUpdate.args[0][1].$push[
  828. this.path.mongo + '.docs'
  829. ],
  830. this.doc
  831. )
  832. return done()
  833. }
  834. )
  835. })
  836. it('should return the project in the callback', function(done) {
  837. return this.subject._putElement(
  838. this.project,
  839. this.folder._id,
  840. this.doc,
  841. 'docs',
  842. (err, path, project) => {
  843. assert.equal(project, this.project)
  844. return done()
  845. }
  846. )
  847. })
  848. it('should add an s onto the type if not included', function(done) {
  849. return this.subject._putElement(
  850. this.project,
  851. this.folder._id,
  852. this.doc,
  853. 'doc',
  854. err => {
  855. assert.deepEqual(
  856. this.ProjectModel.findOneAndUpdate.args[0][1].$push[
  857. this.path.mongo + '.docs'
  858. ],
  859. this.doc
  860. )
  861. return done()
  862. }
  863. )
  864. })
  865. it('should not call update if element is null', function(done) {
  866. return this.subject._putElement(
  867. this.project,
  868. this.folder._id,
  869. null,
  870. 'doc',
  871. err => {
  872. this.ProjectModel.findOneAndUpdate.called.should.equal(false)
  873. return done()
  874. }
  875. )
  876. })
  877. it('should default to root folder insert', function(done) {
  878. return this.subject._putElement(
  879. this.project,
  880. null,
  881. this.doc,
  882. 'doc',
  883. err => {
  884. this.ProjectLocator.findElement.args[0][0].element_id.should.equal(
  885. this.project.rootFolder[0]._id
  886. )
  887. return done()
  888. }
  889. )
  890. })
  891. it('should error if the element has no _id', function(done) {
  892. const doc = { name: 'something' }
  893. return this.subject._putElement(
  894. this.project,
  895. this.folder._id,
  896. doc,
  897. 'doc',
  898. err => {
  899. this.ProjectModel.findOneAndUpdate.called.should.equal(false)
  900. return done()
  901. }
  902. )
  903. })
  904. it('should error if element name contains invalid characters', function(done) {
  905. const doc = {
  906. _id: ObjectId(),
  907. name: 'something*bad'
  908. }
  909. return this.subject._putElement(
  910. this.project,
  911. this.folder._id,
  912. doc,
  913. 'doc',
  914. err => {
  915. this.ProjectModel.findOneAndUpdate.called.should.equal(false)
  916. err.should.deep.equal(
  917. new Errors.InvalidNameError('invalid element name')
  918. )
  919. return done()
  920. }
  921. )
  922. })
  923. it('should error if element name is too long', function(done) {
  924. const doc = {
  925. _id: ObjectId(),
  926. name: new Array(200).join('long-') + 'something'
  927. }
  928. return this.subject._putElement(
  929. this.project,
  930. this.folder._id,
  931. doc,
  932. 'doc',
  933. err => {
  934. this.ProjectModel.findOneAndUpdate.called.should.equal(false)
  935. err.should.deep.equal(
  936. new Errors.InvalidNameError('invalid element name')
  937. )
  938. return done()
  939. }
  940. )
  941. })
  942. it('should error if the folder name is too long', function(done) {
  943. this.path = {
  944. mongo: 'mongo.path',
  945. fileSystem: new Array(200).join('subdir/') + 'foo'
  946. }
  947. this.ProjectLocator.findElement.callsArgWith(
  948. 1,
  949. null,
  950. this.folder,
  951. this.path
  952. )
  953. const doc = {
  954. _id: ObjectId(),
  955. name: 'something'
  956. }
  957. return this.subject._putElement(
  958. this.project,
  959. this.folder._id,
  960. doc,
  961. 'doc',
  962. err => {
  963. this.ProjectModel.findOneAndUpdate.called.should.equal(false)
  964. err.should.deep.equal(new Errors.InvalidNameError('path too long'))
  965. return done()
  966. }
  967. )
  968. })
  969. it('should error if a document already exists with the same name', function(done) {
  970. const doc = {
  971. _id: ObjectId(),
  972. name: 'another-doc.tex'
  973. }
  974. return this.subject._putElement(
  975. this.project,
  976. this.folder,
  977. doc,
  978. 'doc',
  979. err => {
  980. this.ProjectModel.findOneAndUpdate.called.should.equal(false)
  981. err.should.deep.equal(
  982. new Errors.InvalidNameError('file already exists')
  983. )
  984. return done()
  985. }
  986. )
  987. })
  988. it('should error if a file already exists with the same name', function(done) {
  989. const doc = {
  990. _id: ObjectId(),
  991. name: 'another-file.tex'
  992. }
  993. return this.subject._putElement(
  994. this.project,
  995. this.folder,
  996. doc,
  997. 'doc',
  998. err => {
  999. this.ProjectModel.findOneAndUpdate.called.should.equal(false)
  1000. err.should.deep.equal(
  1001. new Errors.InvalidNameError('file already exists')
  1002. )
  1003. return done()
  1004. }
  1005. )
  1006. })
  1007. return it('should error if a folder already exists with the same name', function(done) {
  1008. const doc = {
  1009. _id: ObjectId(),
  1010. name: 'another-folder'
  1011. }
  1012. return this.subject._putElement(
  1013. this.project,
  1014. this.folder,
  1015. doc,
  1016. 'doc',
  1017. err => {
  1018. this.ProjectModel.findOneAndUpdate.called.should.equal(false)
  1019. err.should.deep.equal(
  1020. new Errors.InvalidNameError('file already exists')
  1021. )
  1022. return done()
  1023. }
  1024. )
  1025. })
  1026. })
  1027. })
  1028. describe('_checkValidElementName', function() {
  1029. beforeEach(function() {
  1030. return (this.folder = {
  1031. docs: [{ name: 'doc_name' }],
  1032. fileRefs: [{ name: 'file_name' }],
  1033. folders: [{ name: 'folder_name' }]
  1034. })
  1035. })
  1036. it('returns an error if name matches any doc name', function() {
  1037. return this.subject._checkValidElementName(this.folder, 'doc_name', err =>
  1038. expect(err).to.deep.equal(
  1039. new Errors.InvalidNameError('file already exists')
  1040. )
  1041. )
  1042. })
  1043. it('returns an error if name matches any file name', function() {
  1044. return this.subject._checkValidElementName(
  1045. this.folder,
  1046. 'file_name',
  1047. err =>
  1048. expect(err).to.deep.equal(
  1049. new Errors.InvalidNameError('file already exists')
  1050. )
  1051. )
  1052. })
  1053. it('returns an error if name matches any folder name', function() {
  1054. return this.subject._checkValidElementName(
  1055. this.folder,
  1056. 'folder_name',
  1057. err =>
  1058. expect(err).to.deep.equal(
  1059. new Errors.InvalidNameError('file already exists')
  1060. )
  1061. )
  1062. })
  1063. return it('returns nothing if name is valid', function() {
  1064. return this.subject._checkValidElementName(
  1065. this.folder,
  1066. 'unique_name',
  1067. err => expect(err).to.be.undefined
  1068. )
  1069. })
  1070. })
  1071. describe('_checkValidMove', function() {
  1072. beforeEach(function() {
  1073. this.destFolder = { _id: folder_id }
  1074. this.destFolderPath = { fileSystem: '/foo/bar' }
  1075. this.ProjectLocator.findElement = sinon
  1076. .stub()
  1077. .yields(null, this.destFolder, this.destFolderPath)
  1078. return (this.subject._checkValidElementName = sinon.stub().yields())
  1079. })
  1080. it('checks the element name is valid', function() {
  1081. this.doc = { _id: doc_id, name: 'doc_name' }
  1082. return this.subject._checkValidMove(
  1083. this.project,
  1084. 'doc',
  1085. this.doc,
  1086. { fileSystem: '/main.tex' },
  1087. this.destFolder._id,
  1088. err => {
  1089. expect(err).to.be.undefined
  1090. return this.subject._checkValidElementName
  1091. .calledWith(this.destFolder, this.doc.name)
  1092. .should.equal(true)
  1093. }
  1094. )
  1095. })
  1096. return it('returns an error if trying to move a folder inside itself', function() {
  1097. const folder = { name: 'folder_name' }
  1098. return this.subject._checkValidMove(
  1099. this.project,
  1100. 'folder',
  1101. folder,
  1102. { fileSystem: '/foo' },
  1103. this.destFolder._id,
  1104. err => {
  1105. return expect(err).to.deep.equal(
  1106. new Errors.InvalidNameError(
  1107. 'destination folder is a child folder of me'
  1108. )
  1109. )
  1110. }
  1111. )
  1112. })
  1113. })
  1114. return describe('_insertDeletedDocReference', function() {
  1115. beforeEach(function() {
  1116. this.doc = {
  1117. _id: ObjectId(),
  1118. name: 'test.tex'
  1119. }
  1120. this.callback = sinon.stub()
  1121. this.ProjectModel.update = sinon.stub().yields()
  1122. return this.subject._insertDeletedDocReference(
  1123. project_id,
  1124. this.doc,
  1125. this.callback
  1126. )
  1127. })
  1128. it('should insert the doc into deletedDocs', function() {
  1129. return this.ProjectModel.update
  1130. .calledWith(
  1131. {
  1132. _id: project_id
  1133. },
  1134. {
  1135. $push: {
  1136. deletedDocs: {
  1137. _id: this.doc._id,
  1138. name: this.doc.name,
  1139. deletedAt: new Date()
  1140. }
  1141. }
  1142. }
  1143. )
  1144. .should.equal(true)
  1145. })
  1146. return it('should call the callback', function() {
  1147. return this.callback.called.should.equal(true)
  1148. })
  1149. })
  1150. })