ProjectStructureTests.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. const { expect } = require('chai')
  2. const { ObjectId } = require('mongodb')
  3. const Path = require('path')
  4. const fs = require('fs')
  5. const { Project } = require('../../../app/src/models/Project')
  6. const ProjectGetter = require('../../../app/src/Features/Project/ProjectGetter.js')
  7. const User = require('./helpers/User')
  8. const MockDocStoreApiClass = require('./mocks/MockDocstoreApi')
  9. const MockDocUpdaterApiClass = require('./mocks/MockDocUpdaterApi')
  10. let MockDocStoreApi, MockDocUpdaterApi
  11. before(function () {
  12. MockDocUpdaterApi = MockDocUpdaterApiClass.instance()
  13. MockDocStoreApi = MockDocStoreApiClass.instance()
  14. })
  15. describe('ProjectStructureChanges', function () {
  16. let owner
  17. beforeEach(function (done) {
  18. owner = new User()
  19. owner.login(done)
  20. })
  21. function createExampleProject(owner, callback) {
  22. owner.createProject(
  23. 'example-project',
  24. { template: 'example' },
  25. (error, projectId) => {
  26. if (error) {
  27. return callback(error)
  28. }
  29. ProjectGetter.getProject(projectId, (error, project) => {
  30. if (error) {
  31. return callback(error)
  32. }
  33. const rootFolderId = project.rootFolder[0]._id.toString()
  34. callback(null, projectId, rootFolderId)
  35. })
  36. }
  37. )
  38. }
  39. function createExampleDoc(owner, projectId, callback) {
  40. ProjectGetter.getProject(projectId, (error, project) => {
  41. if (error) {
  42. return callback(error)
  43. }
  44. owner.request.post(
  45. {
  46. uri: `project/${projectId}/doc`,
  47. json: {
  48. name: 'new.tex',
  49. parent_folder_id: project.rootFolder[0]._id,
  50. },
  51. },
  52. (error, res, body) => {
  53. if (error) {
  54. return callback(error)
  55. }
  56. if (res.statusCode < 200 || res.statusCode >= 300) {
  57. return callback(new Error(`failed to add doc ${res.statusCode}`))
  58. }
  59. callback(null, body._id)
  60. }
  61. )
  62. })
  63. }
  64. function createExampleFolder(owner, projectId, callback) {
  65. owner.request.post(
  66. {
  67. uri: `project/${projectId}/folder`,
  68. json: {
  69. name: 'foo',
  70. },
  71. },
  72. (error, res, body) => {
  73. if (error) {
  74. return callback(error)
  75. }
  76. if (res.statusCode < 200 || res.statusCode >= 300) {
  77. return callback(new Error(`failed to add doc ${res.statusCode}`))
  78. }
  79. callback(null, body._id)
  80. }
  81. )
  82. }
  83. function uploadExampleProject(owner, zipFilename, options, callback) {
  84. if (typeof options === 'function') {
  85. callback = options
  86. options = {}
  87. }
  88. const zipFile = fs.createReadStream(
  89. Path.resolve(Path.join(__dirname, '..', 'files', zipFilename))
  90. )
  91. owner.request.post(
  92. {
  93. uri: 'project/new/upload',
  94. formData: {
  95. name: zipFilename,
  96. qqfile: zipFile,
  97. },
  98. },
  99. (error, res, body) => {
  100. if (error) {
  101. return callback(error)
  102. }
  103. if (
  104. !options.allowBadStatus &&
  105. (res.statusCode < 200 || res.statusCode >= 300)
  106. ) {
  107. return new Error(`failed to upload project ${res.statusCode}`)
  108. }
  109. callback(null, JSON.parse(body).project_id, res)
  110. }
  111. )
  112. }
  113. function deleteItem(owner, projectId, type, itemId, callback) {
  114. owner.deleteItemInProject(projectId, type, itemId, callback)
  115. }
  116. describe('uploading a project with a name', function () {
  117. let exampleProjectId
  118. const testProjectName = 'wombat'
  119. beforeEach(function (done) {
  120. uploadExampleProject(
  121. owner,
  122. 'test_project_with_name.zip',
  123. (err, projectId) => {
  124. if (err) {
  125. return done(err)
  126. }
  127. exampleProjectId = projectId
  128. done()
  129. }
  130. )
  131. })
  132. it('should set the project name from the zip contents', function (done) {
  133. ProjectGetter.getProject(exampleProjectId, (error, project) => {
  134. expect(error).not.to.exist
  135. expect(project.name).to.equal(testProjectName)
  136. done()
  137. })
  138. })
  139. })
  140. describe('uploading a project with an invalid name', function () {
  141. let exampleProjectId
  142. const testProjectMatch = /^bad[^\\]+name$/
  143. beforeEach(function (done) {
  144. uploadExampleProject(
  145. owner,
  146. 'test_project_with_invalid_name.zip',
  147. (error, projectId) => {
  148. if (error) {
  149. return done(error)
  150. }
  151. exampleProjectId = projectId
  152. done()
  153. }
  154. )
  155. })
  156. it('should set the project name from the zip contents', function (done) {
  157. ProjectGetter.getProject(exampleProjectId, (error, project) => {
  158. expect(error).not.to.exist
  159. expect(project.name).to.match(testProjectMatch)
  160. done()
  161. })
  162. })
  163. })
  164. describe('uploading an empty zipfile', function () {
  165. let res
  166. beforeEach(function (done) {
  167. uploadExampleProject(
  168. owner,
  169. 'test_project_empty.zip',
  170. { allowBadStatus: true },
  171. (err, projectId, response) => {
  172. if (err) {
  173. return done(err)
  174. }
  175. res = response
  176. done()
  177. }
  178. )
  179. })
  180. it('should fail with 422 error', function () {
  181. expect(res.statusCode).to.equal(422)
  182. })
  183. })
  184. describe('uploading a zipfile containing only empty directories', function () {
  185. let res
  186. beforeEach(function (done) {
  187. uploadExampleProject(
  188. owner,
  189. 'test_project_with_empty_folder.zip',
  190. { allowBadStatus: true },
  191. (err, projectId, response) => {
  192. if (err) {
  193. return done(err)
  194. }
  195. res = response
  196. done()
  197. }
  198. )
  199. })
  200. it('should fail with 422 error', function () {
  201. expect(res.statusCode).to.equal(422)
  202. })
  203. })
  204. describe('uploading a project with a shared top-level folder', function () {
  205. let exampleProjectId
  206. beforeEach(function (done) {
  207. uploadExampleProject(
  208. owner,
  209. 'test_project_with_shared_top_level_folder.zip',
  210. (err, projectId) => {
  211. if (err) {
  212. return done(err)
  213. }
  214. exampleProjectId = projectId
  215. done()
  216. }
  217. )
  218. })
  219. it('should not create the top-level folder', function (done) {
  220. ProjectGetter.getProject(exampleProjectId, (error, project) => {
  221. expect(error).not.to.exist
  222. expect(project.rootFolder[0].folders.length).to.equal(0)
  223. expect(project.rootFolder[0].docs.length).to.equal(2)
  224. done()
  225. })
  226. })
  227. })
  228. describe('uploading a project with backslashes in the path names', function () {
  229. let exampleProjectId
  230. beforeEach(function (done) {
  231. uploadExampleProject(
  232. owner,
  233. 'test_project_with_backslash_in_filename.zip',
  234. (err, projectId) => {
  235. if (err) {
  236. return done(err)
  237. }
  238. exampleProjectId = projectId
  239. done()
  240. }
  241. )
  242. })
  243. it('should treat the backslash as a directory separator', function (done) {
  244. ProjectGetter.getProject(exampleProjectId, (error, project) => {
  245. expect(error).not.to.exist
  246. expect(project.rootFolder[0].folders[0].name).to.equal('styles')
  247. expect(project.rootFolder[0].folders[0].docs[0].name).to.equal('ao.sty')
  248. done()
  249. })
  250. })
  251. })
  252. describe('deleting docs', function () {
  253. beforeEach(function (done) {
  254. createExampleProject(owner, (err, projectId) => {
  255. if (err) {
  256. return done(err)
  257. }
  258. this.exampleProjectId = projectId
  259. createExampleFolder(owner, projectId, (err, folderId) => {
  260. if (err) {
  261. return done(err)
  262. }
  263. this.exampleFolderId = folderId
  264. createExampleDoc(owner, projectId, (err, docId) => {
  265. if (err) {
  266. return done(err)
  267. }
  268. this.exampleDocId = docId
  269. MockDocUpdaterApi.reset()
  270. ProjectGetter.getProject(
  271. this.exampleProjectId,
  272. (error, project) => {
  273. if (error) {
  274. throw error
  275. }
  276. this.project0 = project
  277. done()
  278. }
  279. )
  280. })
  281. })
  282. })
  283. })
  284. it('should pass the doc name to docstore', function (done) {
  285. deleteItem(
  286. owner,
  287. this.exampleProjectId,
  288. 'doc',
  289. this.exampleDocId,
  290. error => {
  291. if (error) return done(error)
  292. expect(
  293. MockDocStoreApi.getDeletedDocs(this.exampleProjectId)
  294. ).to.deep.equal([{ _id: this.exampleDocId, name: 'new.tex' }])
  295. done()
  296. }
  297. )
  298. })
  299. describe('when rootDoc_id matches doc being deleted', function () {
  300. beforeEach(function (done) {
  301. Project.updateOne(
  302. { _id: this.exampleProjectId },
  303. { $set: { rootDoc_id: this.exampleDocId } },
  304. done
  305. )
  306. })
  307. it('should clear rootDoc_id', function (done) {
  308. deleteItem(
  309. owner,
  310. this.exampleProjectId,
  311. 'doc',
  312. this.exampleDocId,
  313. () => {
  314. ProjectGetter.getProject(
  315. this.exampleProjectId,
  316. (error, project) => {
  317. if (error) {
  318. throw error
  319. }
  320. expect(project.rootDoc_id).to.be.undefined
  321. done()
  322. }
  323. )
  324. }
  325. )
  326. })
  327. })
  328. describe('when rootDoc_id does not match doc being deleted', function () {
  329. beforeEach(function (done) {
  330. this.exampleRootDocId = new ObjectId()
  331. Project.updateOne(
  332. { _id: this.exampleProjectId },
  333. { $set: { rootDoc_id: this.exampleRootDocId } },
  334. done
  335. )
  336. })
  337. it('should not clear rootDoc_id', function (done) {
  338. deleteItem(
  339. owner,
  340. this.exampleProjectId,
  341. 'doc',
  342. this.exampleDocId,
  343. () => {
  344. ProjectGetter.getProject(
  345. this.exampleProjectId,
  346. (error, project) => {
  347. if (error) {
  348. throw error
  349. }
  350. expect(project.rootDoc_id.toString()).to.equal(
  351. this.exampleRootDocId.toString()
  352. )
  353. done()
  354. }
  355. )
  356. }
  357. )
  358. })
  359. })
  360. })
  361. })