ProjectStructureTests.mjs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. import { 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 { fileURLToPath } from 'node:url'
  11. const User = UserHelper.promises
  12. const ObjectId = mongodb.ObjectId
  13. const __dirname = fileURLToPath(new URL('.', import.meta.url))
  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(__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 docs', function () {
  170. beforeEach(async function () {
  171. const { projectId } = await createExampleProject(owner)
  172. this.exampleProjectId = projectId
  173. const folderId = await createExampleFolder(owner, projectId)
  174. this.exampleFolderId = folderId
  175. const docId = await createExampleDoc(owner, projectId)
  176. this.exampleDocId = docId
  177. MockDocUpdaterApi.reset()
  178. const project = await ProjectGetter.promises.getProject(
  179. this.exampleProjectId
  180. )
  181. this.project0 = project
  182. })
  183. it('should pass the doc name to docstore', async function () {
  184. await deleteItem(owner, this.exampleProjectId, 'doc', this.exampleDocId)
  185. expect(
  186. MockDocStoreApi.getDeletedDocs(this.exampleProjectId)
  187. ).to.deep.equal([{ _id: this.exampleDocId, name: 'new.tex' }])
  188. })
  189. describe('when rootDoc_id matches doc being deleted', function () {
  190. beforeEach(async function () {
  191. await Project.updateOne(
  192. { _id: this.exampleProjectId },
  193. { $set: { rootDoc_id: this.exampleDocId } }
  194. ).exec()
  195. })
  196. it('should clear rootDoc_id', async function () {
  197. await deleteItem(owner, this.exampleProjectId, 'doc', this.exampleDocId)
  198. const project = ProjectGetter.promises.getProject(this.exampleProjectId)
  199. expect(project.rootDoc_id).to.be.undefined
  200. })
  201. })
  202. describe('when rootDoc_id does not match doc being deleted', function () {
  203. beforeEach(async function () {
  204. this.exampleRootDocId = new ObjectId()
  205. await Project.updateOne(
  206. { _id: this.exampleProjectId },
  207. { $set: { rootDoc_id: this.exampleRootDocId } }
  208. ).exec()
  209. })
  210. it('should not clear rootDoc_id', async function () {
  211. await deleteItem(owner, this.exampleProjectId, 'doc', this.exampleDocId)
  212. const project = await ProjectGetter.promises.getProject(
  213. this.exampleProjectId
  214. )
  215. expect(project.rootDoc_id.toString()).to.equal(
  216. this.exampleRootDocId.toString()
  217. )
  218. })
  219. })
  220. })
  221. })