ProjectStructureTests.js 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453
  1. const { expect } = require('chai')
  2. const mkdirp = require('mkdirp')
  3. const { ObjectId } = require('mongojs')
  4. const Path = require('path')
  5. const fs = require('fs')
  6. const Settings = require('settings-sharelatex')
  7. const _ = require('underscore')
  8. const { Project } = require('../../../app/src/models/Project')
  9. const ProjectGetter = require('../../../app/src/Features/Project/ProjectGetter.js')
  10. const MockDocUpdaterApi = require('./helpers/MockDocUpdaterApi')
  11. require('./helpers/MockFileStoreApi')
  12. require('./helpers/MockProjectHistoryApi')
  13. const User = require('./helpers/User')
  14. describe('ProjectStructureChanges', function() {
  15. let owner
  16. beforeEach(function(done) {
  17. owner = new User()
  18. owner.login(done)
  19. })
  20. function createExampleProject(owner, callback) {
  21. owner.createProject(
  22. 'example-project',
  23. { template: 'example' },
  24. (error, projectId) => {
  25. if (error) {
  26. return callback(error)
  27. }
  28. ProjectGetter.getProject(projectId, (error, project) => {
  29. if (error) {
  30. return callback(error)
  31. }
  32. const rootFolderId = project.rootFolder[0]._id.toString()
  33. callback(null, projectId, rootFolderId)
  34. })
  35. }
  36. )
  37. }
  38. function createExampleDoc(owner, projectId, callback) {
  39. ProjectGetter.getProject(projectId, (error, project) => {
  40. if (error) {
  41. return callback(error)
  42. }
  43. owner.request.post(
  44. {
  45. uri: `project/${projectId}/doc`,
  46. json: {
  47. name: 'new.tex',
  48. parent_folder_id: project.rootFolder[0]._id
  49. }
  50. },
  51. (error, res, body) => {
  52. if (error) {
  53. return callback(error)
  54. }
  55. if (res.statusCode < 200 || res.statusCode >= 300) {
  56. return callback(new Error(`failed to add doc ${res.statusCode}`))
  57. }
  58. callback(null, body._id)
  59. }
  60. )
  61. })
  62. }
  63. function createExampleFolder(owner, projectId, callback) {
  64. owner.request.post(
  65. {
  66. uri: `project/${projectId}/folder`,
  67. json: {
  68. name: 'foo'
  69. }
  70. },
  71. (error, res, body) => {
  72. if (error) {
  73. return callback(error)
  74. }
  75. if (res.statusCode < 200 || res.statusCode >= 300) {
  76. return callback(new Error(`failed to add doc ${res.statusCode}`))
  77. }
  78. callback(null, body._id)
  79. }
  80. )
  81. }
  82. function uploadFile(
  83. owner,
  84. projectId,
  85. folderId,
  86. file,
  87. name,
  88. contentType,
  89. callback
  90. ) {
  91. const imageFile = fs.createReadStream(
  92. Path.resolve(Path.join(__dirname, '..', 'files', file))
  93. )
  94. owner.request.post(
  95. {
  96. uri: `project/${projectId}/upload`,
  97. qs: {
  98. folder_id: folderId
  99. },
  100. formData: {
  101. qqfile: {
  102. value: imageFile,
  103. options: {
  104. filename: name,
  105. contentType: contentType
  106. }
  107. }
  108. }
  109. },
  110. (error, res, body) => {
  111. if (error) {
  112. return callback(error)
  113. }
  114. if (res.statusCode < 200 || res.statusCode >= 300) {
  115. return callback(new Error(`failed to upload file ${res.statusCode}`))
  116. }
  117. callback(null, JSON.parse(body).entity_id)
  118. }
  119. )
  120. }
  121. function uploadExampleFile(owner, projectId, folderId, callback) {
  122. uploadFile(
  123. owner,
  124. projectId,
  125. folderId,
  126. '1pixel.png',
  127. '1pixel.png',
  128. 'image/png',
  129. callback
  130. )
  131. }
  132. function uploadExampleProject(owner, zipFilename, options, callback) {
  133. if (typeof options === 'function') {
  134. callback = options
  135. options = {}
  136. }
  137. const zipFile = fs.createReadStream(
  138. Path.resolve(Path.join(__dirname, '..', 'files', zipFilename))
  139. )
  140. owner.request.post(
  141. {
  142. uri: 'project/new/upload',
  143. formData: {
  144. qqfile: zipFile
  145. }
  146. },
  147. (error, res, body) => {
  148. if (error) {
  149. return callback(error)
  150. }
  151. if (
  152. !options.allowBadStatus &&
  153. (res.statusCode < 200 || res.statusCode >= 300)
  154. ) {
  155. return new Error(`failed to upload project ${res.statusCode}`)
  156. }
  157. callback(null, JSON.parse(body).project_id, res)
  158. }
  159. )
  160. }
  161. function moveItem(owner, projectId, type, itemId, folderId, callback) {
  162. owner.request.post(
  163. {
  164. uri: `project/${projectId}/${type}/${itemId}/move`,
  165. json: {
  166. folder_id: folderId
  167. }
  168. },
  169. (error, res) => {
  170. if (error) {
  171. return callback(error)
  172. }
  173. if (res.statusCode < 200 || res.statusCode >= 300) {
  174. return callback(new Error(`failed to move ${type} ${res.statusCode}`))
  175. }
  176. callback()
  177. }
  178. )
  179. }
  180. function renameItem(owner, projectId, type, itemId, name, callback) {
  181. owner.request.post(
  182. {
  183. uri: `project/${projectId}/${type}/${itemId}/rename`,
  184. json: {
  185. name: name
  186. }
  187. },
  188. (error, res) => {
  189. if (error) {
  190. return callback(error)
  191. }
  192. if (res.statusCode < 200 || res.statusCode >= 300) {
  193. return callback(
  194. new Error(`failed to rename ${type} ${res.statusCode}`)
  195. )
  196. }
  197. callback()
  198. }
  199. )
  200. }
  201. function deleteItem(owner, projectId, type, itemId, callback) {
  202. owner.request.delete(
  203. {
  204. uri: `project/${projectId}/${type}/${itemId}`
  205. },
  206. (error, res) => {
  207. if (error) {
  208. return callback(error)
  209. }
  210. if (res.statusCode < 200 || res.statusCode >= 300) {
  211. return callback(
  212. new Error(`failed to delete folder ${res.statusCode}`)
  213. )
  214. }
  215. callback()
  216. }
  217. )
  218. }
  219. function verifyVersionIncremented(
  220. projectId,
  221. oldVersion,
  222. updateVersion,
  223. increment,
  224. callback
  225. ) {
  226. expect(updateVersion).to.equal(oldVersion + increment)
  227. ProjectGetter.getProject(projectId, (error, newProject) => {
  228. if (error) {
  229. return callback(error)
  230. }
  231. expect(newProject.version).to.equal(updateVersion)
  232. callback()
  233. })
  234. }
  235. describe('creating a project from the example template', function() {
  236. let exampleProjectId
  237. beforeEach(function(done) {
  238. MockDocUpdaterApi.clearProjectStructureUpdates()
  239. createExampleProject(owner, (err, projectId) => {
  240. exampleProjectId = projectId
  241. done(err)
  242. })
  243. })
  244. it('should version creating a doc', function() {
  245. const {
  246. docUpdates: updates,
  247. version
  248. } = MockDocUpdaterApi.getProjectStructureUpdates(exampleProjectId)
  249. expect(updates.length).to.equal(2)
  250. _.each(updates, update => {
  251. expect(update.userId).to.equal(owner._id)
  252. expect(update.docLines).to.be.a('string')
  253. })
  254. expect(_.where(updates, { pathname: '/main.tex' }).length).to.equal(1)
  255. expect(_.where(updates, { pathname: '/references.bib' }).length).to.equal(
  256. 1
  257. )
  258. expect(version).to.equal(3)
  259. })
  260. it('should version creating a file', function() {
  261. const {
  262. fileUpdates: updates,
  263. version
  264. } = MockDocUpdaterApi.getProjectStructureUpdates(exampleProjectId)
  265. expect(updates.length).to.equal(1)
  266. const update = updates[0]
  267. expect(update.userId).to.equal(owner._id)
  268. expect(update.pathname).to.equal('/universe.jpg')
  269. expect(update.url).to.be.a('string')
  270. expect(version).to.equal(3)
  271. })
  272. })
  273. describe('duplicating a project', function() {
  274. let dupProjectId
  275. beforeEach(function(done) {
  276. MockDocUpdaterApi.clearProjectStructureUpdates()
  277. createExampleProject(owner, (err, projectId) => {
  278. if (err) {
  279. return done(err)
  280. }
  281. owner.request.post(
  282. {
  283. uri: `/Project/${projectId}/clone`,
  284. json: {
  285. projectName: 'new.tex'
  286. }
  287. },
  288. (error, res, body) => {
  289. if (error) {
  290. throw error
  291. }
  292. if (res.statusCode < 200 || res.statusCode >= 300) {
  293. throw new Error(`failed to clone project ${res.statusCode}`)
  294. }
  295. dupProjectId = body.project_id
  296. done()
  297. }
  298. )
  299. })
  300. })
  301. it('should version the docs created', function() {
  302. const {
  303. docUpdates: updates,
  304. version
  305. } = MockDocUpdaterApi.getProjectStructureUpdates(dupProjectId)
  306. expect(updates.length).to.equal(2)
  307. _.each(updates, update => {
  308. expect(update.userId).to.equal(owner._id)
  309. expect(update.docLines).to.be.a('string')
  310. })
  311. expect(_.where(updates, { pathname: '/main.tex' }).length).to.equal(1)
  312. expect(_.where(updates, { pathname: '/references.bib' }).length).to.equal(
  313. 1
  314. )
  315. expect(version).to.equal(3)
  316. })
  317. it('should version the files created', function() {
  318. const {
  319. fileUpdates: updates,
  320. version
  321. } = MockDocUpdaterApi.getProjectStructureUpdates(dupProjectId)
  322. expect(updates.length).to.equal(1)
  323. const update = updates[0]
  324. expect(update.userId).to.equal(owner._id)
  325. expect(update.pathname).to.equal('/universe.jpg')
  326. expect(update.url).to.be.a('string')
  327. expect(version).to.equal(3)
  328. })
  329. })
  330. describe('adding a doc', function() {
  331. let exampleProjectId, oldVersion
  332. beforeEach(function(done) {
  333. createExampleProject(owner, (err, projectId) => {
  334. if (err) {
  335. return done(err)
  336. }
  337. exampleProjectId = projectId
  338. MockDocUpdaterApi.clearProjectStructureUpdates()
  339. ProjectGetter.getProject(projectId, (error, project) => {
  340. if (error) {
  341. return done(error)
  342. }
  343. oldVersion = project.version
  344. createExampleDoc(owner, projectId, done)
  345. })
  346. })
  347. })
  348. it('should version the doc added', function(done) {
  349. const {
  350. docUpdates: updates,
  351. version: newVersion
  352. } = MockDocUpdaterApi.getProjectStructureUpdates(exampleProjectId)
  353. expect(updates.length).to.equal(1)
  354. const update = updates[0]
  355. expect(update.userId).to.equal(owner._id)
  356. expect(update.pathname).to.equal('/new.tex')
  357. expect(update.docLines).to.be.a('string')
  358. verifyVersionIncremented(
  359. exampleProjectId,
  360. oldVersion,
  361. newVersion,
  362. 1,
  363. done
  364. )
  365. })
  366. })
  367. describe('uploading a project', function() {
  368. let exampleProjectId
  369. beforeEach(function(done) {
  370. uploadExampleProject(owner, 'test_project.zip', (err, projectId) => {
  371. if (err) {
  372. return done(err)
  373. }
  374. exampleProjectId = projectId
  375. done()
  376. })
  377. })
  378. it('should version the docs created', function() {
  379. const {
  380. docUpdates: updates,
  381. version
  382. } = MockDocUpdaterApi.getProjectStructureUpdates(exampleProjectId)
  383. expect(updates.length).to.equal(1)
  384. const update = updates[0]
  385. expect(update.userId).to.equal(owner._id)
  386. expect(update.pathname).to.equal('/main.tex')
  387. expect(update.docLines).to.equal('Test')
  388. expect(version).to.equal(2)
  389. })
  390. it('should version the files created', function() {
  391. const {
  392. fileUpdates: updates,
  393. version
  394. } = MockDocUpdaterApi.getProjectStructureUpdates(exampleProjectId)
  395. expect(updates.length).to.equal(1)
  396. const update = updates[0]
  397. expect(update.userId).to.equal(owner._id)
  398. expect(update.pathname).to.equal('/1pixel.png')
  399. expect(update.url).to.be.a('string')
  400. expect(version).to.equal(2)
  401. })
  402. })
  403. describe('uploading a project with a name', function() {
  404. let exampleProjectId
  405. const testProjectName = 'wombat'
  406. beforeEach(function(done) {
  407. uploadExampleProject(
  408. owner,
  409. 'test_project_with_name.zip',
  410. (err, projectId) => {
  411. if (err) {
  412. return done(err)
  413. }
  414. exampleProjectId = projectId
  415. done()
  416. }
  417. )
  418. })
  419. it('should set the project name from the zip contents', function(done) {
  420. ProjectGetter.getProject(exampleProjectId, (error, project) => {
  421. expect(error).not.to.exist
  422. expect(project.name).to.equal(testProjectName)
  423. done()
  424. })
  425. })
  426. })
  427. describe('uploading a project with an invalid name', function() {
  428. let exampleProjectId
  429. const testProjectMatch = /^bad[^\\]+name$/
  430. beforeEach(function(done) {
  431. uploadExampleProject(
  432. owner,
  433. 'test_project_with_invalid_name.zip',
  434. (error, projectId) => {
  435. if (error) {
  436. return done(error)
  437. }
  438. exampleProjectId = projectId
  439. done()
  440. }
  441. )
  442. })
  443. it('should set the project name from the zip contents', function(done) {
  444. ProjectGetter.getProject(exampleProjectId, (error, project) => {
  445. expect(error).not.to.exist
  446. expect(project.name).to.match(testProjectMatch)
  447. done()
  448. })
  449. })
  450. })
  451. describe('uploading an empty zipfile', function() {
  452. let res
  453. beforeEach(function(done) {
  454. uploadExampleProject(
  455. owner,
  456. 'test_project_empty.zip',
  457. { allowBadStatus: true },
  458. (err, projectId, response) => {
  459. if (err) {
  460. return done(err)
  461. }
  462. res = response
  463. done()
  464. }
  465. )
  466. })
  467. it('should fail with 422 error', function() {
  468. expect(res.statusCode).to.equal(422)
  469. })
  470. })
  471. describe('uploading a zipfile containing only empty directories', function() {
  472. let res
  473. beforeEach(function(done) {
  474. uploadExampleProject(
  475. owner,
  476. 'test_project_with_empty_folder.zip',
  477. { allowBadStatus: true },
  478. (err, projectId, response) => {
  479. if (err) {
  480. return done(err)
  481. }
  482. res = response
  483. done()
  484. }
  485. )
  486. })
  487. it('should fail with 422 error', function() {
  488. expect(res.statusCode).to.equal(422)
  489. })
  490. })
  491. describe('uploading a project with a shared top-level folder', function() {
  492. let exampleProjectId
  493. beforeEach(function(done) {
  494. uploadExampleProject(
  495. owner,
  496. 'test_project_with_shared_top_level_folder.zip',
  497. (err, projectId) => {
  498. if (err) {
  499. return done(err)
  500. }
  501. exampleProjectId = projectId
  502. done()
  503. }
  504. )
  505. })
  506. it('should not create the top-level folder', function(done) {
  507. ProjectGetter.getProject(exampleProjectId, (error, project) => {
  508. expect(error).not.to.exist
  509. expect(project.rootFolder[0].folders.length).to.equal(0)
  510. expect(project.rootFolder[0].docs.length).to.equal(2)
  511. done()
  512. })
  513. })
  514. })
  515. describe('uploading a project with backslashes in the path names', function() {
  516. let exampleProjectId
  517. beforeEach(function(done) {
  518. uploadExampleProject(
  519. owner,
  520. 'test_project_with_backslash_in_filename.zip',
  521. (err, projectId) => {
  522. if (err) {
  523. return done(err)
  524. }
  525. exampleProjectId = projectId
  526. done()
  527. }
  528. )
  529. })
  530. it('should treat the backslash as a directory separator', function(done) {
  531. ProjectGetter.getProject(exampleProjectId, (error, project) => {
  532. expect(error).not.to.exist
  533. expect(project.rootFolder[0].folders[0].name).to.equal('styles')
  534. expect(project.rootFolder[0].folders[0].docs[0].name).to.equal('ao.sty')
  535. done()
  536. })
  537. })
  538. })
  539. describe('uploading a project with files in different encodings', function() {
  540. let docUpdates
  541. beforeEach(function(done) {
  542. uploadExampleProject(owner, 'charsets/charsets.zip', (err, projectId) => {
  543. if (err) {
  544. return done(err)
  545. }
  546. docUpdates = MockDocUpdaterApi.getProjectStructureUpdates(projectId)
  547. .docUpdates
  548. done()
  549. })
  550. })
  551. it('should correctly parse windows-1252', function() {
  552. const update = _.find(
  553. docUpdates,
  554. update => update.pathname === '/test-german-windows-1252.tex'
  555. )
  556. expect(update.docLines).to.contain(
  557. 'Der schnelle braune Fuchs sprang träge über den Hund.'
  558. )
  559. })
  560. it('should correctly parse German utf8', function() {
  561. const update = _.find(
  562. docUpdates,
  563. update => update.pathname === '/test-german-utf8x.tex'
  564. )
  565. expect(update.docLines).to.contain(
  566. 'Der schnelle braune Fuchs sprang träge über den Hund.'
  567. )
  568. })
  569. it('should correctly parse little-endian utf16', function() {
  570. const update = _.find(
  571. docUpdates,
  572. update => update.pathname === '/test-greek-utf16-le-bom.tex'
  573. )
  574. expect(update.docLines).to.contain(
  575. 'Η γρήγορη καστανή αλεπού πήδηξε χαλαρά πάνω από το σκυλί.'
  576. )
  577. })
  578. it('should correctly parse Greek utf8', function() {
  579. const update = _.find(
  580. docUpdates,
  581. update => update.pathname === '/test-greek-utf8x.tex'
  582. )
  583. expect(update.docLines).to.contain(
  584. 'Η γρήγορη καστανή αλεπού πήδηξε χαλαρά πάνω από το σκυλί.'
  585. )
  586. })
  587. })
  588. describe('uploading a file', function() {
  589. let exampleProjectId, oldVersion, rootFolderId
  590. beforeEach(function(done) {
  591. createExampleProject(owner, (err, projectId, folderId) => {
  592. if (err) {
  593. return done(err)
  594. }
  595. exampleProjectId = projectId
  596. rootFolderId = folderId
  597. MockDocUpdaterApi.clearProjectStructureUpdates()
  598. ProjectGetter.getProject(projectId, (error, project) => {
  599. if (error) {
  600. throw error
  601. }
  602. oldVersion = project.version
  603. uploadExampleFile(owner, projectId, rootFolderId, done)
  604. })
  605. })
  606. })
  607. it('should version a newly uploaded file', function(done) {
  608. const {
  609. fileUpdates: updates,
  610. version
  611. } = MockDocUpdaterApi.getProjectStructureUpdates(exampleProjectId)
  612. expect(updates.length).to.equal(1)
  613. const update = updates[0]
  614. expect(update.userId).to.equal(owner._id)
  615. expect(update.pathname).to.equal('/1pixel.png')
  616. expect(update.url).to.be.a('string')
  617. // one file upload
  618. verifyVersionIncremented(exampleProjectId, oldVersion, version, 1, done)
  619. })
  620. it('should version a replacement file', function(done) {
  621. MockDocUpdaterApi.clearProjectStructureUpdates()
  622. uploadFile(
  623. owner,
  624. exampleProjectId,
  625. rootFolderId,
  626. '2pixel.png',
  627. '1pixel.png',
  628. 'image/png',
  629. () => {
  630. const {
  631. fileUpdates: updates,
  632. version
  633. } = MockDocUpdaterApi.getProjectStructureUpdates(exampleProjectId)
  634. expect(updates.length).to.equal(2)
  635. let update = updates[0]
  636. expect(update.userId).to.equal(owner._id)
  637. expect(update.pathname).to.equal('/1pixel.png')
  638. update = updates[1]
  639. expect(update.userId).to.equal(owner._id)
  640. expect(update.pathname).to.equal('/1pixel.png')
  641. expect(update.url).to.be.a('string')
  642. // two file uploads
  643. verifyVersionIncremented(
  644. exampleProjectId,
  645. oldVersion,
  646. version,
  647. 2,
  648. done
  649. )
  650. }
  651. )
  652. })
  653. })
  654. describe('moving entities', function() {
  655. let exampleProjectId,
  656. oldVersion,
  657. exampleDocId,
  658. exampleFileId,
  659. exampleFolderId
  660. beforeEach(function(done) {
  661. createExampleProject(owner, (err, projectId, rootFolderId) => {
  662. if (err) {
  663. return done(err)
  664. }
  665. exampleProjectId = projectId
  666. createExampleDoc(owner, projectId, (err, docId) => {
  667. if (err) {
  668. return done(err)
  669. }
  670. exampleDocId = docId
  671. uploadExampleFile(owner, projectId, rootFolderId, (err, fileId) => {
  672. if (err) {
  673. return done(err)
  674. }
  675. exampleFileId = fileId
  676. createExampleFolder(owner, projectId, (err, folderId) => {
  677. if (err) {
  678. return done(err)
  679. }
  680. exampleFolderId = folderId
  681. ProjectGetter.getProject(projectId, (error, project) => {
  682. if (error) {
  683. throw error
  684. }
  685. oldVersion = project.version
  686. MockDocUpdaterApi.clearProjectStructureUpdates()
  687. done()
  688. })
  689. })
  690. })
  691. })
  692. })
  693. })
  694. it('should version moving a doc', function(done) {
  695. moveItem(
  696. owner,
  697. exampleProjectId,
  698. 'doc',
  699. exampleDocId,
  700. exampleFolderId,
  701. () => {
  702. const {
  703. docUpdates: updates,
  704. version
  705. } = MockDocUpdaterApi.getProjectStructureUpdates(exampleProjectId)
  706. expect(updates.length).to.equal(1)
  707. const update = updates[0]
  708. expect(update.userId).to.equal(owner._id)
  709. expect(update.pathname).to.equal('/new.tex')
  710. expect(update.newPathname).to.equal('/foo/new.tex')
  711. // 2, because it's a delete and then add
  712. verifyVersionIncremented(
  713. exampleProjectId,
  714. oldVersion,
  715. version,
  716. 2,
  717. done
  718. )
  719. }
  720. )
  721. })
  722. it('should version moving a file', function(done) {
  723. moveItem(
  724. owner,
  725. exampleProjectId,
  726. 'file',
  727. exampleFileId,
  728. exampleFolderId,
  729. () => {
  730. const {
  731. fileUpdates: updates,
  732. version
  733. } = MockDocUpdaterApi.getProjectStructureUpdates(exampleProjectId)
  734. expect(updates.length).to.equal(1)
  735. const update = updates[0]
  736. expect(update.userId).to.equal(owner._id)
  737. expect(update.pathname).to.equal('/1pixel.png')
  738. expect(update.newPathname).to.equal('/foo/1pixel.png')
  739. // 2, because it's a delete and then add
  740. verifyVersionIncremented(
  741. exampleProjectId,
  742. oldVersion,
  743. version,
  744. 2,
  745. done
  746. )
  747. }
  748. )
  749. })
  750. it('should version moving a folder', function(done) {
  751. moveItem(
  752. owner,
  753. exampleProjectId,
  754. 'doc',
  755. exampleDocId,
  756. exampleFolderId,
  757. () => {
  758. MockDocUpdaterApi.clearProjectStructureUpdates()
  759. owner.request.post(
  760. {
  761. uri: `project/${exampleProjectId}/folder`,
  762. json: {
  763. name: 'bar'
  764. }
  765. },
  766. (error, res, body) => {
  767. if (error) {
  768. throw error
  769. }
  770. const newFolderId = body._id
  771. moveItem(
  772. owner,
  773. exampleProjectId,
  774. 'folder',
  775. exampleFolderId,
  776. newFolderId,
  777. () => {
  778. const {
  779. docUpdates: updates,
  780. version
  781. } = MockDocUpdaterApi.getProjectStructureUpdates(
  782. exampleProjectId
  783. )
  784. expect(updates.length).to.equal(1)
  785. let update = updates[0]
  786. expect(update.userId).to.equal(owner._id)
  787. expect(update.pathname).to.equal('/foo/new.tex')
  788. expect(update.newPathname).to.equal('/bar/foo/new.tex')
  789. // 5, because it's two file moves plus a folder
  790. verifyVersionIncremented(
  791. exampleProjectId,
  792. oldVersion,
  793. version,
  794. 5,
  795. done
  796. )
  797. }
  798. )
  799. }
  800. )
  801. }
  802. )
  803. })
  804. })
  805. describe('renaming entities', function() {
  806. let exampleProjectId,
  807. exampleDocId,
  808. exampleFileId,
  809. exampleFolderId,
  810. oldVersion
  811. beforeEach(function(done) {
  812. createExampleProject(owner, (err, projectId, rootFolderId) => {
  813. if (err) {
  814. return done(err)
  815. }
  816. exampleProjectId = projectId
  817. createExampleDoc(owner, projectId, (err, docId) => {
  818. if (err) {
  819. return done(err)
  820. }
  821. exampleDocId = docId
  822. uploadExampleFile(owner, projectId, rootFolderId, (err, fileId) => {
  823. if (err) {
  824. return done(err)
  825. }
  826. exampleFileId = fileId
  827. createExampleFolder(owner, projectId, (err, folderId) => {
  828. if (err) {
  829. return done(err)
  830. }
  831. exampleFolderId = folderId
  832. moveItem(owner, projectId, 'doc', docId, folderId, () => {
  833. moveItem(owner, projectId, 'file', fileId, folderId, () => {
  834. MockDocUpdaterApi.clearProjectStructureUpdates()
  835. ProjectGetter.getProject(
  836. exampleProjectId,
  837. (error, project) => {
  838. if (error) {
  839. throw error
  840. }
  841. oldVersion = project.version
  842. done()
  843. }
  844. )
  845. })
  846. })
  847. })
  848. })
  849. })
  850. })
  851. })
  852. it('should version renaming a doc', function(done) {
  853. renameItem(
  854. owner,
  855. exampleProjectId,
  856. 'Doc',
  857. exampleDocId,
  858. 'wombat.tex',
  859. () => {
  860. const {
  861. docUpdates: updates,
  862. version
  863. } = MockDocUpdaterApi.getProjectStructureUpdates(exampleProjectId)
  864. expect(updates.length).to.equal(1)
  865. const update = updates[0]
  866. expect(update.userId).to.equal(owner._id)
  867. expect(update.pathname).to.equal('/foo/new.tex')
  868. expect(update.newPathname).to.equal('/foo/wombat.tex')
  869. verifyVersionIncremented(
  870. exampleProjectId,
  871. oldVersion,
  872. version,
  873. 1,
  874. done
  875. )
  876. }
  877. )
  878. })
  879. it('should version renaming a file', function(done) {
  880. renameItem(
  881. owner,
  882. exampleProjectId,
  883. 'file',
  884. exampleFileId,
  885. 'potato.png',
  886. () => {
  887. const {
  888. fileUpdates: updates,
  889. version
  890. } = MockDocUpdaterApi.getProjectStructureUpdates(exampleProjectId)
  891. expect(updates.length).to.equal(1)
  892. const update = updates[0]
  893. expect(update.userId).to.equal(owner._id)
  894. expect(update.pathname).to.equal('/foo/1pixel.png')
  895. expect(update.newPathname).to.equal('/foo/potato.png')
  896. verifyVersionIncremented(
  897. exampleProjectId,
  898. oldVersion,
  899. version,
  900. 1,
  901. done
  902. )
  903. }
  904. )
  905. })
  906. it('should version renaming a folder', function(done) {
  907. renameItem(
  908. owner,
  909. exampleProjectId,
  910. 'folder',
  911. exampleFolderId,
  912. 'giraffe',
  913. () => {
  914. const {
  915. docUpdates,
  916. fileUpdates,
  917. version
  918. } = MockDocUpdaterApi.getProjectStructureUpdates(exampleProjectId)
  919. expect(docUpdates.length).to.equal(1)
  920. const docUpdate = docUpdates[0]
  921. expect(docUpdate.userId).to.equal(owner._id)
  922. expect(docUpdate.pathname).to.equal('/foo/new.tex')
  923. expect(docUpdate.newPathname).to.equal('/giraffe/new.tex')
  924. expect(fileUpdates.length).to.equal(1)
  925. const fileUpdate = fileUpdates[0]
  926. expect(fileUpdate.userId).to.equal(owner._id)
  927. expect(fileUpdate.pathname).to.equal('/foo/1pixel.png')
  928. expect(fileUpdate.newPathname).to.equal('/giraffe/1pixel.png')
  929. verifyVersionIncremented(
  930. exampleProjectId,
  931. oldVersion,
  932. version,
  933. 1,
  934. done
  935. )
  936. }
  937. )
  938. })
  939. })
  940. describe('deleting entities', function() {
  941. let exampleProjectId, oldVersion, exampleFolderId
  942. beforeEach(function(done) {
  943. createExampleProject(owner, (err, projectId) => {
  944. if (err) {
  945. return done(err)
  946. }
  947. exampleProjectId = projectId
  948. createExampleFolder(owner, exampleProjectId, (err, folderId) => {
  949. if (err) {
  950. return done(err)
  951. }
  952. exampleFolderId = folderId
  953. createExampleDoc(owner, projectId, (err, docId) => {
  954. if (err) {
  955. return done(err)
  956. }
  957. uploadExampleFile(owner, projectId, folderId, (err, fileId) => {
  958. if (err) {
  959. return done(err)
  960. }
  961. moveItem(owner, projectId, 'doc', docId, folderId, () => {
  962. moveItem(owner, projectId, 'file', fileId, folderId, () => {
  963. MockDocUpdaterApi.clearProjectStructureUpdates()
  964. ProjectGetter.getProject(
  965. exampleProjectId,
  966. (error, project) => {
  967. if (error) {
  968. throw error
  969. }
  970. oldVersion = project.version
  971. done()
  972. }
  973. )
  974. })
  975. })
  976. })
  977. })
  978. })
  979. })
  980. })
  981. it('should version deleting a folder', function(done) {
  982. deleteItem(owner, exampleProjectId, 'folder', exampleFolderId, () => {
  983. const {
  984. docUpdates,
  985. fileUpdates,
  986. version
  987. } = MockDocUpdaterApi.getProjectStructureUpdates(exampleProjectId)
  988. expect(docUpdates.length).to.equal(1)
  989. const docUpdate = docUpdates[0]
  990. expect(docUpdate.userId).to.equal(owner._id)
  991. expect(docUpdate.pathname).to.equal('/foo/new.tex')
  992. expect(docUpdate.newPathname).to.equal('')
  993. expect(fileUpdates.length).to.equal(1)
  994. const fileUpdate = fileUpdates[0]
  995. expect(fileUpdate.userId).to.equal(owner._id)
  996. expect(fileUpdate.pathname).to.equal('/foo/1pixel.png')
  997. expect(fileUpdate.newPathname).to.equal('')
  998. verifyVersionIncremented(exampleProjectId, oldVersion, version, 1, done)
  999. })
  1000. })
  1001. })
  1002. describe('deleting docs', function() {
  1003. beforeEach(function(done) {
  1004. createExampleProject(this, () => {
  1005. createExampleFolder(this, () => {
  1006. createExampleDoc(this, () => {
  1007. MockDocUpdaterApi.clearProjectStructureUpdates()
  1008. ProjectGetter.getProject(
  1009. this.exampleProjectId,
  1010. (error, project) => {
  1011. if (error) {
  1012. throw error
  1013. }
  1014. this.project0 = project
  1015. done()
  1016. }
  1017. )
  1018. })
  1019. })
  1020. })
  1021. })
  1022. describe('when rootDoc_id matches doc being deleted', function() {
  1023. beforeEach(function(done) {
  1024. Project.update(
  1025. { _id: this.exampleProjectId },
  1026. { $set: { rootDoc_id: this.exampleDocId } },
  1027. done
  1028. )
  1029. })
  1030. it('should clear rootDoc_id', function(done) {
  1031. deleteItem(this, 'doc', this.exampleDocId, () => {
  1032. ProjectGetter.getProject(this.exampleProjectId, (error, project) => {
  1033. if (error) {
  1034. throw error
  1035. }
  1036. expect(project.rootDoc_id).to.be.undefined
  1037. done()
  1038. })
  1039. })
  1040. })
  1041. })
  1042. describe('when rootDoc_id does not match doc being deleted', function() {
  1043. beforeEach(function(done) {
  1044. this.exampleRootDocId = new ObjectId()
  1045. Project.update(
  1046. { _id: this.exampleProjectId },
  1047. { $set: { rootDoc_id: this.exampleRootDocId } },
  1048. done
  1049. )
  1050. })
  1051. it('should not clear rootDoc_id', function(done) {
  1052. deleteItem(this, 'doc', this.exampleDocId, () => {
  1053. ProjectGetter.getProject(this.exampleProjectId, (error, project) => {
  1054. if (error) {
  1055. throw error
  1056. }
  1057. expect(project.rootDoc_id.toString()).to.equal(
  1058. this.exampleRootDocId.toString()
  1059. )
  1060. done()
  1061. })
  1062. })
  1063. })
  1064. })
  1065. })
  1066. describe('tpds', function() {
  1067. let projectName, exampleProjectId, oldVersion, rootFolderId
  1068. beforeEach(function(done) {
  1069. projectName = `tpds-project-${new ObjectId().toString()}`
  1070. owner.createProject(projectName, (error, projectId) => {
  1071. if (error) {
  1072. throw error
  1073. }
  1074. exampleProjectId = projectId
  1075. mkdirp(Settings.path.dumpFolder, () => {
  1076. ProjectGetter.getProject(exampleProjectId, (error, project) => {
  1077. if (error) {
  1078. throw error
  1079. }
  1080. MockDocUpdaterApi.clearProjectStructureUpdates()
  1081. rootFolderId = project.rootFolder[0]._id.toString()
  1082. oldVersion = project.version
  1083. done()
  1084. })
  1085. })
  1086. })
  1087. })
  1088. it('should version adding a doc', function(done) {
  1089. const texFile = fs.createReadStream(
  1090. Path.resolve(Path.join(__dirname, '..', 'files', 'test.tex'))
  1091. )
  1092. const req = owner.request.post({
  1093. uri: `/user/${owner._id}/update/${projectName}/test.tex`,
  1094. auth: {
  1095. user: _.keys(Settings.httpAuthUsers)[0],
  1096. pass: _.values(Settings.httpAuthUsers)[0],
  1097. sendImmediately: true
  1098. }
  1099. })
  1100. texFile.on('error', err => {
  1101. throw err
  1102. })
  1103. req.on('error', err => {
  1104. throw err
  1105. })
  1106. req.on('response', res => {
  1107. if (res.statusCode < 200 || res.statusCode >= 300) {
  1108. throw new Error(`failed to upload file ${res.statusCode}`)
  1109. }
  1110. const {
  1111. docUpdates: updates,
  1112. version
  1113. } = MockDocUpdaterApi.getProjectStructureUpdates(exampleProjectId)
  1114. expect(updates.length).to.equal(1)
  1115. const update = updates[0]
  1116. expect(update.userId).to.equal(owner._id)
  1117. expect(update.pathname).to.equal('/test.tex')
  1118. expect(update.docLines).to.equal('Test')
  1119. verifyVersionIncremented(exampleProjectId, oldVersion, version, 1, done)
  1120. })
  1121. texFile.pipe(req)
  1122. })
  1123. it('should version adding a new file', function(done) {
  1124. const imageFile = fs.createReadStream(
  1125. Path.resolve(Path.join(__dirname, '..', 'files', '1pixel.png'))
  1126. )
  1127. const req = owner.request.post({
  1128. uri: `/user/${owner._id}/update/${projectName}/1pixel.png`,
  1129. auth: {
  1130. user: _.keys(Settings.httpAuthUsers)[0],
  1131. pass: _.values(Settings.httpAuthUsers)[0],
  1132. sendImmediately: true
  1133. }
  1134. })
  1135. imageFile.on('error', err => {
  1136. throw err
  1137. })
  1138. req.on('error', err => {
  1139. throw err
  1140. })
  1141. req.on('response', res => {
  1142. if (res.statusCode < 200 || res.statusCode >= 300) {
  1143. throw new Error(`failed to upload file ${res.statusCode}`)
  1144. }
  1145. const {
  1146. fileUpdates: updates,
  1147. version
  1148. } = MockDocUpdaterApi.getProjectStructureUpdates(exampleProjectId)
  1149. expect(updates.length).to.equal(1)
  1150. const update = updates[0]
  1151. expect(update.userId).to.equal(owner._id)
  1152. expect(update.pathname).to.equal('/1pixel.png')
  1153. expect(update.url).to.be.a('string')
  1154. verifyVersionIncremented(exampleProjectId, oldVersion, version, 1, done)
  1155. })
  1156. imageFile.pipe(req)
  1157. })
  1158. describe('when there are files in the project', function() {
  1159. beforeEach(function(done) {
  1160. uploadExampleFile(owner, exampleProjectId, rootFolderId, () => {
  1161. createExampleDoc(owner, exampleProjectId, () => {
  1162. ProjectGetter.getProject(exampleProjectId, (error, project) => {
  1163. if (error) {
  1164. throw error
  1165. }
  1166. MockDocUpdaterApi.clearProjectStructureUpdates()
  1167. oldVersion = project.version
  1168. done()
  1169. })
  1170. })
  1171. })
  1172. })
  1173. it('should version replacing a file', function(done) {
  1174. const imageFile = fs.createReadStream(
  1175. Path.resolve(Path.join(__dirname, '..', 'files', '2pixel.png'))
  1176. )
  1177. const req = owner.request.post({
  1178. uri: `/user/${owner._id}/update/${projectName}/1pixel.png`,
  1179. auth: {
  1180. user: _.keys(Settings.httpAuthUsers)[0],
  1181. pass: _.values(Settings.httpAuthUsers)[0],
  1182. sendImmediately: true
  1183. }
  1184. })
  1185. imageFile.on('error', err => {
  1186. throw err
  1187. })
  1188. req.on('error', err => {
  1189. throw err
  1190. })
  1191. req.on('response', res => {
  1192. if (res.statusCode < 200 || res.statusCode >= 300) {
  1193. throw new Error(`failed to upload file ${res.statusCode}`)
  1194. }
  1195. const {
  1196. fileUpdates: updates,
  1197. version
  1198. } = MockDocUpdaterApi.getProjectStructureUpdates(exampleProjectId)
  1199. expect(updates.length).to.equal(2)
  1200. let update = updates[0]
  1201. expect(update.userId).to.equal(owner._id)
  1202. expect(update.pathname).to.equal('/1pixel.png')
  1203. // expect(update.url).to.be.a('string');
  1204. update = updates[1]
  1205. expect(update.userId).to.equal(owner._id)
  1206. expect(update.pathname).to.equal('/1pixel.png')
  1207. expect(update.url).to.be.a('string')
  1208. verifyVersionIncremented(
  1209. exampleProjectId,
  1210. oldVersion,
  1211. version,
  1212. 1,
  1213. done
  1214. )
  1215. })
  1216. imageFile.pipe(req)
  1217. })
  1218. it('should version deleting a doc', function(done) {
  1219. owner.request.delete(
  1220. {
  1221. uri: `/user/${owner._id}/update/${projectName}/new.tex`,
  1222. auth: {
  1223. user: _.keys(Settings.httpAuthUsers)[0],
  1224. pass: _.values(Settings.httpAuthUsers)[0],
  1225. sendImmediately: true
  1226. }
  1227. },
  1228. (error, res) => {
  1229. if (error) {
  1230. throw error
  1231. }
  1232. if (res.statusCode < 200 || res.statusCode >= 300) {
  1233. throw new Error(`failed to delete doc ${res.statusCode}`)
  1234. }
  1235. const {
  1236. docUpdates: updates,
  1237. version
  1238. } = MockDocUpdaterApi.getProjectStructureUpdates(exampleProjectId)
  1239. expect(updates.length).to.equal(1)
  1240. const update = updates[0]
  1241. expect(update.userId).to.equal(owner._id)
  1242. expect(update.pathname).to.equal('/new.tex')
  1243. expect(update.newPathname).to.equal('')
  1244. verifyVersionIncremented(
  1245. exampleProjectId,
  1246. oldVersion,
  1247. version,
  1248. 1,
  1249. done
  1250. )
  1251. }
  1252. )
  1253. })
  1254. })
  1255. })
  1256. describe('uploading a document', function() {
  1257. let exampleProjectId, rootFolderId
  1258. beforeEach(function(done) {
  1259. createExampleProject(owner, (err, projectId, folderId) => {
  1260. if (err) {
  1261. return done(err)
  1262. }
  1263. exampleProjectId = projectId
  1264. rootFolderId = folderId
  1265. MockDocUpdaterApi.clearProjectStructureUpdates()
  1266. done()
  1267. })
  1268. })
  1269. describe('with an unusual character set', function() {
  1270. it('should correctly handle utf16-le data', function(done) {
  1271. uploadFile(
  1272. owner,
  1273. exampleProjectId,
  1274. rootFolderId,
  1275. 'charsets/test-greek-utf16-le-bom.tex',
  1276. 'test-greek-utf16-le-bom.tex',
  1277. 'text/x-tex',
  1278. () => {
  1279. const {
  1280. docUpdates: updates
  1281. } = MockDocUpdaterApi.getProjectStructureUpdates(exampleProjectId)
  1282. expect(updates.length).to.equal(1)
  1283. const update = updates[0]
  1284. expect(update.pathname).to.equal('/test-greek-utf16-le-bom.tex')
  1285. expect(update.docLines).to.contain(
  1286. 'Η γρήγορη καστανή αλεπού πήδηξε χαλαρά πάνω από το σκυλί.'
  1287. )
  1288. done()
  1289. }
  1290. )
  1291. })
  1292. it('should correctly handle windows1252/iso-8859-1/latin1 data', function(done) {
  1293. uploadFile(
  1294. owner,
  1295. exampleProjectId,
  1296. rootFolderId,
  1297. 'charsets/test-german-windows-1252.tex',
  1298. 'test-german-windows-1252.tex',
  1299. 'text/x-tex',
  1300. () => {
  1301. const {
  1302. docUpdates: updates
  1303. } = MockDocUpdaterApi.getProjectStructureUpdates(exampleProjectId)
  1304. expect(updates.length).to.equal(1)
  1305. const update = updates[0]
  1306. expect(update.pathname).to.equal('/test-german-windows-1252.tex')
  1307. expect(update.docLines).to.contain(
  1308. 'Der schnelle braune Fuchs sprang träge über den Hund.'
  1309. )
  1310. done()
  1311. }
  1312. )
  1313. })
  1314. })
  1315. })
  1316. })