ProjectStructureTests.mjs 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. import chai, { expect } from 'chai'
  2. import mongodb from 'mongodb-legacy'
  3. import Path from 'node:path'
  4. import fs from 'node:fs'
  5. import { Project } from '../../../app/src/models/Project.js'
  6. import ProjectGetter from '../../../app/src/Features/Project/ProjectGetter.js'
  7. import UserHelper from './helpers/User.mjs'
  8. import MockDocStoreApiClass from './mocks/MockDocstoreApi.mjs'
  9. import MockDocUpdaterApiClass from './mocks/MockDocUpdaterApi.mjs'
  10. import chaiAsPromised from 'chai-as-promised'
  11. chai.use(chaiAsPromised)
  12. const User = UserHelper.promises
  13. const ObjectId = mongodb.ObjectId
  14. let MockDocStoreApi, MockDocUpdaterApi
  15. before(function () {
  16. MockDocUpdaterApi = MockDocUpdaterApiClass.instance()
  17. MockDocStoreApi = MockDocStoreApiClass.instance()
  18. })
  19. describe('ProjectStructureChanges', function () {
  20. let owner
  21. beforeEach(async function () {
  22. owner = new User()
  23. await owner.login()
  24. })
  25. async function createExampleProject(owner) {
  26. const projectId = await owner.createProject('example-project', {
  27. template: 'example',
  28. })
  29. const project = await ProjectGetter.promises.getProject(projectId)
  30. const rootFolderId = project.rootFolder[0]._id.toString()
  31. return { projectId, rootFolderId }
  32. }
  33. async function createExampleDoc(owner, projectId) {
  34. const project = await ProjectGetter.promises.getProject(projectId)
  35. const { response, body } = await owner.doRequest('POST', {
  36. uri: `project/${projectId}/doc`,
  37. json: {
  38. name: 'new.tex',
  39. parent_folder_id: project.rootFolder[0]._id,
  40. },
  41. })
  42. if (response.statusCode < 200 || response.statusCode >= 300) {
  43. throw new Error(`failed to add doc ${response.statusCode}`)
  44. }
  45. return body._id
  46. }
  47. async function createExampleFolder(owner, projectId) {
  48. const { response, body } = await owner.doRequest('POST', {
  49. uri: `project/${projectId}/folder`,
  50. json: {
  51. name: 'foo',
  52. },
  53. })
  54. if (response.statusCode < 200 || response.statusCode >= 300) {
  55. throw new Error(`failed to add doc ${response.statusCode}`)
  56. }
  57. return body._id
  58. }
  59. async function uploadExampleProject(owner, zipFilename, options = {}) {
  60. const zipFile = fs.createReadStream(
  61. Path.resolve(Path.join(import.meta.dirname, '..', 'files', zipFilename))
  62. )
  63. const { response, body } = await owner.doRequest('POST', {
  64. uri: 'project/new/upload',
  65. formData: {
  66. name: zipFilename,
  67. qqfile: zipFile,
  68. },
  69. })
  70. if (
  71. !options.allowBadStatus &&
  72. (response.statusCode < 200 || response.statusCode >= 300)
  73. ) {
  74. throw new Error(`failed to upload project ${response.statusCode}`)
  75. }
  76. return { projectId: JSON.parse(body).project_id, response }
  77. }
  78. async function deleteItem(owner, projectId, type, itemId) {
  79. return await owner.deleteItemInProject(projectId, type, itemId)
  80. }
  81. describe('uploading a project with a name', function () {
  82. let exampleProjectId
  83. const testProjectName = 'wombat'
  84. beforeEach(async function () {
  85. const { projectId } = await uploadExampleProject(
  86. owner,
  87. 'test_project_with_name.zip'
  88. )
  89. exampleProjectId = projectId
  90. })
  91. it('should set the project name from the zip contents', async function () {
  92. const project = await ProjectGetter.promises.getProject(exampleProjectId)
  93. expect(project.name).to.equal(testProjectName)
  94. })
  95. })
  96. describe('uploading a project with an invalid name', function () {
  97. let exampleProjectId
  98. const testProjectMatch = /^bad[^\\]+name$/
  99. beforeEach(async function () {
  100. const { projectId } = await uploadExampleProject(
  101. owner,
  102. 'test_project_with_invalid_name.zip'
  103. )
  104. exampleProjectId = projectId
  105. })
  106. it('should set the project name from the zip contents', async function () {
  107. const project = await ProjectGetter.promises.getProject(exampleProjectId)
  108. expect(project.name).to.match(testProjectMatch)
  109. })
  110. })
  111. describe('uploading an empty zipfile', function () {
  112. let res
  113. beforeEach(async function () {
  114. const { response } = await uploadExampleProject(
  115. owner,
  116. 'test_project_empty.zip',
  117. { allowBadStatus: true }
  118. )
  119. res = response
  120. })
  121. it('should fail with 422 error', function () {
  122. expect(res.statusCode).to.equal(422)
  123. })
  124. })
  125. describe('uploading a zipfile containing only empty directories', function () {
  126. let res
  127. beforeEach(async function () {
  128. const { response } = await uploadExampleProject(
  129. owner,
  130. 'test_project_with_empty_folder.zip',
  131. { allowBadStatus: true }
  132. )
  133. res = response
  134. })
  135. it('should fail with 422 error', function () {
  136. expect(res.statusCode).to.equal(422)
  137. })
  138. })
  139. describe('uploading a project with a shared top-level folder', function () {
  140. let exampleProjectId
  141. beforeEach(async function () {
  142. const { projectId } = await uploadExampleProject(
  143. owner,
  144. 'test_project_with_shared_top_level_folder.zip'
  145. )
  146. exampleProjectId = projectId
  147. })
  148. it('should not create the top-level folder', async function () {
  149. const project = await ProjectGetter.promises.getProject(exampleProjectId)
  150. expect(project.rootFolder[0].folders.length).to.equal(0)
  151. expect(project.rootFolder[0].docs.length).to.equal(2)
  152. })
  153. })
  154. describe('uploading a project with backslashes in the path names', function () {
  155. let exampleProjectId
  156. beforeEach(async function () {
  157. const { projectId } = await uploadExampleProject(
  158. owner,
  159. 'test_project_with_backslash_in_filename.zip'
  160. )
  161. exampleProjectId = projectId
  162. })
  163. it('should treat the backslash as a directory separator', async function () {
  164. const project = await ProjectGetter.promises.getProject(exampleProjectId)
  165. expect(project.rootFolder[0].folders[0].name).to.equal('styles')
  166. expect(project.rootFolder[0].folders[0].docs[0].name).to.equal('ao.sty')
  167. })
  168. })
  169. describe('deleting folders', function () {
  170. beforeEach(async function () {
  171. const { projectId } = await createExampleProject(owner)
  172. this.exampleProjectId = projectId
  173. })
  174. describe('when the folder is the rootFolder', function () {
  175. beforeEach(async function () {
  176. const project = await ProjectGetter.promises.getProject(
  177. this.exampleProjectId
  178. )
  179. this.rootFolderId = project.rootFolder[0]._id
  180. })
  181. it('should fail with a 422 error', async function () {
  182. await expect(
  183. deleteItem(owner, this.exampleProjectId, 'folder', this.rootFolderId)
  184. )
  185. .to.be.rejected.and.eventually.match(/status=422/)
  186. .and.eventually.match(/body="cannot delete root folder"/)
  187. })
  188. })
  189. describe('when the folder is not the rootFolder', function () {
  190. beforeEach(async function () {
  191. const folderId = await createExampleFolder(owner, this.exampleProjectId)
  192. this.exampleFolderId = folderId
  193. })
  194. it('should succeed', async function () {
  195. await expect(
  196. deleteItem(
  197. owner,
  198. this.exampleProjectId,
  199. 'folder',
  200. this.exampleFolderId
  201. )
  202. ).to.be.fulfilled
  203. })
  204. })
  205. })
  206. describe('deleting docs', function () {
  207. beforeEach(async function () {
  208. const { projectId } = await createExampleProject(owner)
  209. this.exampleProjectId = projectId
  210. const folderId = await createExampleFolder(owner, projectId)
  211. this.exampleFolderId = folderId
  212. const docId = await createExampleDoc(owner, projectId)
  213. this.exampleDocId = docId
  214. MockDocUpdaterApi.reset()
  215. const project = await ProjectGetter.promises.getProject(
  216. this.exampleProjectId
  217. )
  218. this.project0 = project
  219. })
  220. it('should pass the doc name to docstore', async function () {
  221. await deleteItem(owner, this.exampleProjectId, 'doc', this.exampleDocId)
  222. expect(
  223. MockDocStoreApi.getDeletedDocs(this.exampleProjectId)
  224. ).to.deep.equal([{ _id: this.exampleDocId, name: 'new.tex' }])
  225. })
  226. describe('when rootDoc_id matches doc being deleted', function () {
  227. beforeEach(async function () {
  228. await Project.updateOne(
  229. { _id: this.exampleProjectId },
  230. { $set: { rootDoc_id: this.exampleDocId } }
  231. ).exec()
  232. })
  233. it('should clear rootDoc_id', async function () {
  234. await deleteItem(owner, this.exampleProjectId, 'doc', this.exampleDocId)
  235. const project = ProjectGetter.promises.getProject(this.exampleProjectId)
  236. expect(project.rootDoc_id).to.be.undefined
  237. })
  238. })
  239. describe('when rootDoc_id does not match doc being deleted', function () {
  240. beforeEach(async function () {
  241. this.exampleRootDocId = new ObjectId()
  242. await Project.updateOne(
  243. { _id: this.exampleProjectId },
  244. { $set: { rootDoc_id: this.exampleRootDocId } }
  245. ).exec()
  246. })
  247. it('should not clear rootDoc_id', async function () {
  248. await deleteItem(owner, this.exampleProjectId, 'doc', this.exampleDocId)
  249. const project = await ProjectGetter.promises.getProject(
  250. this.exampleProjectId
  251. )
  252. expect(project.rootDoc_id.toString()).to.equal(
  253. this.exampleRootDocId.toString()
  254. )
  255. })
  256. })
  257. })
  258. })