ProjectStructureTests.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  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.mjs'
  6. import ProjectGetter from '../../../app/src/Features/Project/ProjectGetter.mjs'
  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('when sending an upload request without a file', function () {
  112. describe('project', function () {
  113. it('should reject the request with status 400', async function () {
  114. const { response, body } = await owner.doRequest('POST', {
  115. uri: 'project/new/upload',
  116. json: true,
  117. formData: {
  118. name: 'foo',
  119. },
  120. })
  121. expect(response.statusCode).to.equal(400)
  122. expect(body).to.deep.equal({
  123. success: false,
  124. error: 'invalid_upload_request',
  125. })
  126. })
  127. })
  128. describe('file', function () {
  129. it('should reject the request with status 400', async function () {
  130. const projectId = await owner.createProject('foo', {
  131. template: 'blank',
  132. })
  133. const { response, body } = await owner.doRequest('POST', {
  134. uri: `project/${projectId}/upload`,
  135. json: true,
  136. formData: {
  137. name: 'foo.txt',
  138. },
  139. })
  140. expect(response.statusCode).to.equal(400)
  141. expect(body).to.deep.equal({
  142. success: false,
  143. error: 'invalid_upload_request',
  144. })
  145. })
  146. })
  147. })
  148. describe('uploading an empty zipfile', function () {
  149. let res
  150. beforeEach(async function () {
  151. const { response } = await uploadExampleProject(
  152. owner,
  153. 'test_project_empty.zip',
  154. { allowBadStatus: true }
  155. )
  156. res = response
  157. })
  158. it('should fail with 422 error', function () {
  159. expect(res.statusCode).to.equal(422)
  160. })
  161. })
  162. describe('uploading a zipfile containing only empty directories', function () {
  163. let res
  164. beforeEach(async function () {
  165. const { response } = await uploadExampleProject(
  166. owner,
  167. 'test_project_with_empty_folder.zip',
  168. { allowBadStatus: true }
  169. )
  170. res = response
  171. })
  172. it('should fail with 422 error', function () {
  173. expect(res.statusCode).to.equal(422)
  174. })
  175. })
  176. describe('uploading a project with a shared top-level folder', function () {
  177. let exampleProjectId
  178. beforeEach(async function () {
  179. const { projectId } = await uploadExampleProject(
  180. owner,
  181. 'test_project_with_shared_top_level_folder.zip'
  182. )
  183. exampleProjectId = projectId
  184. })
  185. it('should not create the top-level folder', async function () {
  186. const project = await ProjectGetter.promises.getProject(exampleProjectId)
  187. expect(project.rootFolder[0].folders.length).to.equal(0)
  188. expect(project.rootFolder[0].docs.length).to.equal(2)
  189. })
  190. })
  191. describe('uploading a project with backslashes in the path names', function () {
  192. let exampleProjectId
  193. beforeEach(async function () {
  194. const { projectId } = await uploadExampleProject(
  195. owner,
  196. 'test_project_with_backslash_in_filename.zip'
  197. )
  198. exampleProjectId = projectId
  199. })
  200. it('should treat the backslash as a directory separator', async function () {
  201. const project = await ProjectGetter.promises.getProject(exampleProjectId)
  202. expect(project.rootFolder[0].folders[0].name).to.equal('styles')
  203. expect(project.rootFolder[0].folders[0].docs[0].name).to.equal('ao.sty')
  204. })
  205. })
  206. describe('uploading a project containing a filename that is too long', function () {
  207. let res
  208. beforeEach(async function () {
  209. const { response } = await uploadExampleProject(
  210. owner,
  211. 'test_project_with_too_long_filename.zip',
  212. { allowBadStatus: true }
  213. )
  214. res = response
  215. })
  216. it('should fail with 422 error', function () {
  217. expect(res.statusCode).to.equal(422)
  218. })
  219. })
  220. describe('deleting folders', function () {
  221. beforeEach(async function () {
  222. const { projectId } = await createExampleProject(owner)
  223. this.exampleProjectId = projectId
  224. })
  225. describe('when the folder is the rootFolder', function () {
  226. beforeEach(async function () {
  227. const project = await ProjectGetter.promises.getProject(
  228. this.exampleProjectId
  229. )
  230. this.rootFolderId = project.rootFolder[0]._id
  231. })
  232. it('should fail with a 422 error', async function () {
  233. await expect(
  234. deleteItem(owner, this.exampleProjectId, 'folder', this.rootFolderId)
  235. )
  236. .to.be.rejected.and.eventually.match(/status=422/)
  237. .and.eventually.match(/body="cannot delete root folder"/)
  238. })
  239. })
  240. describe('when the folder is not the rootFolder', function () {
  241. beforeEach(async function () {
  242. const folderId = await createExampleFolder(owner, this.exampleProjectId)
  243. this.exampleFolderId = folderId
  244. })
  245. it('should succeed', async function () {
  246. await expect(
  247. deleteItem(
  248. owner,
  249. this.exampleProjectId,
  250. 'folder',
  251. this.exampleFolderId
  252. )
  253. ).to.be.fulfilled
  254. })
  255. })
  256. })
  257. describe('deleting docs', function () {
  258. beforeEach(async function () {
  259. const { projectId } = await createExampleProject(owner)
  260. this.exampleProjectId = projectId
  261. const folderId = await createExampleFolder(owner, projectId)
  262. this.exampleFolderId = folderId
  263. const docId = await createExampleDoc(owner, projectId)
  264. this.exampleDocId = docId
  265. MockDocUpdaterApi.reset()
  266. const project = await ProjectGetter.promises.getProject(
  267. this.exampleProjectId
  268. )
  269. this.project0 = project
  270. })
  271. it('should pass the doc name to docstore', async function () {
  272. await deleteItem(owner, this.exampleProjectId, 'doc', this.exampleDocId)
  273. expect(
  274. MockDocStoreApi.getDeletedDocs(this.exampleProjectId)
  275. ).to.deep.equal([{ _id: this.exampleDocId, name: 'new.tex' }])
  276. })
  277. describe('when rootDoc_id matches doc being deleted', function () {
  278. beforeEach(async function () {
  279. await Project.updateOne(
  280. { _id: this.exampleProjectId },
  281. { $set: { rootDoc_id: this.exampleDocId } }
  282. ).exec()
  283. })
  284. it('should clear rootDoc_id', async function () {
  285. await deleteItem(owner, this.exampleProjectId, 'doc', this.exampleDocId)
  286. const project = ProjectGetter.promises.getProject(this.exampleProjectId)
  287. expect(project.rootDoc_id).to.be.undefined
  288. })
  289. })
  290. describe('when rootDoc_id does not match doc being deleted', function () {
  291. beforeEach(async function () {
  292. this.exampleRootDocId = new ObjectId()
  293. await Project.updateOne(
  294. { _id: this.exampleProjectId },
  295. { $set: { rootDoc_id: this.exampleRootDocId } }
  296. ).exec()
  297. })
  298. it('should not clear rootDoc_id', async function () {
  299. await deleteItem(owner, this.exampleProjectId, 'doc', this.exampleDocId)
  300. const project = await ProjectGetter.promises.getProject(
  301. this.exampleProjectId
  302. )
  303. expect(project.rootDoc_id.toString()).to.equal(
  304. this.exampleRootDocId.toString()
  305. )
  306. })
  307. })
  308. })
  309. })