LinkedFilesTests.mjs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. import { expect } from 'chai'
  2. import _ from 'lodash'
  3. import fs from 'fs'
  4. import timekeeper from 'timekeeper'
  5. import Settings from '@overleaf/settings'
  6. import UserHelper from './helpers/User.js'
  7. import express from 'express'
  8. import { plainTextResponse } from '../../../app/src/infrastructure/Response.js'
  9. const User = UserHelper.promises
  10. const LinkedUrlProxy = express()
  11. LinkedUrlProxy.get('/', (req, res, next) => {
  12. if (req.query.url === 'http://example.com/foo') {
  13. return plainTextResponse(res, 'foo foo foo')
  14. } else if (req.query.url === 'http://example.com/bar') {
  15. return plainTextResponse(res, 'bar bar bar')
  16. } else if (req.query.url === 'http://example.com/large') {
  17. return plainTextResponse(res, 'x'.repeat(Settings.maxUploadSize + 1))
  18. } else {
  19. return res.sendStatus(404)
  20. }
  21. })
  22. describe('LinkedFiles', function () {
  23. before(function () {
  24. timekeeper.freeze(new Date())
  25. })
  26. after(function () {
  27. timekeeper.reset()
  28. })
  29. let projectOne, projectOneId, projectOneRootFolderId
  30. let projectTwo, projectTwoId, projectTwoRootFolderId
  31. const sourceDocName = 'test.txt'
  32. let sourceDocId
  33. let owner
  34. let server
  35. before(function (done) {
  36. server = LinkedUrlProxy.listen(6543, done)
  37. })
  38. after(function (done) {
  39. server.close(done)
  40. })
  41. beforeEach(async function () {
  42. owner = new User()
  43. await owner.login()
  44. await fs.promises.mkdir(Settings.path.dumpFolder, { recursive: true })
  45. })
  46. describe('creating a project linked file', function () {
  47. beforeEach(async function () {
  48. projectOneId = await owner.createProject('plf-test-one', {
  49. template: 'blank',
  50. })
  51. projectOne = await owner.getProject(projectOneId)
  52. projectOneRootFolderId = projectOne.rootFolder[0]._id.toString()
  53. projectTwoId = await owner.createProject('plf-test-two', {
  54. template: 'blank',
  55. })
  56. projectTwo = await owner.getProject(projectTwoId)
  57. projectTwoRootFolderId = projectTwo.rootFolder[0]._id.toString()
  58. sourceDocId = await owner.createDocInProject(
  59. projectTwoId,
  60. projectTwoRootFolderId,
  61. sourceDocName
  62. )
  63. await owner.createDocInProject(
  64. projectTwoId,
  65. projectTwoRootFolderId,
  66. 'some-harmless-doc.txt'
  67. )
  68. })
  69. it('should produce a list of the users projects and their entities', async function () {
  70. let { body } = await owner.doRequest('get', {
  71. url: '/user/projects',
  72. json: true,
  73. })
  74. expect(body).to.deep.equal({
  75. projects: [
  76. {
  77. _id: projectOneId,
  78. name: 'plf-test-one',
  79. accessLevel: 'owner',
  80. },
  81. {
  82. _id: projectTwoId,
  83. name: 'plf-test-two',
  84. accessLevel: 'owner',
  85. },
  86. ],
  87. })
  88. ;({ body } = await owner.doRequest('get', {
  89. url: `/project/${projectTwoId}/entities`,
  90. json: true,
  91. }))
  92. expect(body).to.deep.equal({
  93. project_id: projectTwoId,
  94. entities: [
  95. { path: '/main.tex', type: 'doc' },
  96. { path: '/some-harmless-doc.txt', type: 'doc' },
  97. { path: '/test.txt', type: 'doc' },
  98. ],
  99. })
  100. })
  101. it('should import a file and refresh it if there is no v1 id', async function () {
  102. // import the file from the source project
  103. let { response, body } = await owner.doRequest('post', {
  104. url: `/project/${projectOneId}/linked_file`,
  105. json: {
  106. name: 'test-link.txt',
  107. parent_folder_id: projectOneRootFolderId,
  108. provider: 'project_file',
  109. data: {
  110. source_project_id: projectTwoId,
  111. source_entity_path: `/${sourceDocName}`,
  112. },
  113. },
  114. })
  115. expect(response.statusCode).to.equal(200)
  116. const existingFileId = body.new_file_id
  117. expect(existingFileId).to.exist
  118. let updatedProjectOne = await owner.getProject(projectOneId)
  119. let firstFile = updatedProjectOne.rootFolder[0].fileRefs[0]
  120. expect(firstFile._id.toString()).to.equal(existingFileId.toString())
  121. expect(firstFile.linkedFileData).to.deep.equal({
  122. provider: 'project_file',
  123. source_project_id: projectTwoId,
  124. source_entity_path: `/${sourceDocName}`,
  125. importedAt: new Date().toISOString(),
  126. })
  127. expect(firstFile.name).to.equal('test-link.txt')
  128. // refresh the file
  129. ;({ response, body } = await owner.doRequest('post', {
  130. url: `/project/${projectOneId}/linked_file/${existingFileId}/refresh`,
  131. json: true,
  132. }))
  133. expect(response.statusCode).to.equal(200)
  134. const newFileId = body.new_file_id
  135. expect(newFileId).to.exist
  136. expect(newFileId).to.not.equal(existingFileId)
  137. updatedProjectOne = await owner.getProject(projectOneId)
  138. firstFile = updatedProjectOne.rootFolder[0].fileRefs[0]
  139. expect(firstFile._id.toString()).to.equal(newFileId.toString())
  140. expect(firstFile.name).to.equal('test-link.txt')
  141. // should not work if there is a v1 id
  142. ;({ response, body } = await owner.doRequest('post', {
  143. url: `/project/${projectOneId}/linked_file`,
  144. json: {
  145. name: 'test-link-should-not-work.txt',
  146. parent_folder_id: projectOneRootFolderId,
  147. provider: 'project_file',
  148. data: {
  149. v1_source_doc_id: 1234,
  150. source_entity_path: `/${sourceDocName}`,
  151. },
  152. },
  153. }))
  154. expect(response.statusCode).to.equal(403)
  155. expect(body).to.equal(
  156. 'The project that contains this file is not shared with you'
  157. )
  158. })
  159. it('should generate a proper error message when the source file has been deleted', async function () {
  160. // import the file from the source project
  161. let { response, body } = await owner.doRequest('post', {
  162. url: `/project/${projectOneId}/linked_file`,
  163. json: {
  164. name: 'test-link.txt',
  165. parent_folder_id: projectOneRootFolderId,
  166. provider: 'project_file',
  167. data: {
  168. source_project_id: projectTwoId,
  169. source_entity_path: `/${sourceDocName}`,
  170. },
  171. },
  172. })
  173. expect(response.statusCode).to.equal(200)
  174. const existingFileId = body.new_file_id
  175. expect(existingFileId).to.exist
  176. // rename the source file
  177. await owner.renameItemInProject(
  178. projectTwoId,
  179. 'doc',
  180. sourceDocId,
  181. 'renamed-doc.txt'
  182. )
  183. // refresh the file
  184. ;({ response, body } = await owner.doRequest('post', {
  185. url: `/project/${projectOneId}/linked_file/${existingFileId}/refresh`,
  186. json: true,
  187. }))
  188. expect(response.statusCode).to.equal(404)
  189. expect(body).to.equal('Source file not found')
  190. })
  191. })
  192. describe('with a linked project_file from a v1 project that has not been imported', function () {
  193. beforeEach(async function () {
  194. projectOneId = await owner.createProject('plf-v1-test-one', {
  195. template: 'blank',
  196. })
  197. projectOne = await owner.getProject(projectOneId)
  198. projectOneRootFolderId = projectOne.rootFolder[0]._id.toString()
  199. projectOne.rootFolder[0].fileRefs.push({
  200. linkedFileData: {
  201. provider: 'project_file',
  202. v1_source_doc_id: 9999999, // We won't find this id in the database
  203. source_entity_path: 'example.jpeg',
  204. },
  205. _id: 'abcd',
  206. rev: 0,
  207. created: new Date(),
  208. name: 'example.jpeg',
  209. })
  210. await owner.saveProject(projectOne)
  211. })
  212. it('should refuse to refresh', async function () {
  213. const { response, body } = await owner.doRequest('post', {
  214. url: `/project/${projectOneId}/linked_file/abcd/refresh`,
  215. json: true,
  216. })
  217. expect(response.statusCode).to.equal(409)
  218. expect(body).to.equal(
  219. 'Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file'
  220. )
  221. })
  222. })
  223. describe('creating a URL based linked file', function () {
  224. beforeEach(async function () {
  225. projectOneId = await owner.createProject('url-linked-files-project', {
  226. template: 'blank',
  227. })
  228. projectOne = await owner.getProject(projectOneId)
  229. projectOneRootFolderId = projectOne.rootFolder[0]._id.toString()
  230. })
  231. it('should download, create and replace a file', async function () {
  232. // downloading the initial file
  233. let { response, body } = await owner.doRequest('post', {
  234. url: `/project/${projectOneId}/linked_file`,
  235. json: {
  236. provider: 'url',
  237. data: {
  238. url: 'http://example.com/foo',
  239. },
  240. parent_folder_id: projectOneRootFolderId,
  241. name: 'url-test-file-1',
  242. },
  243. })
  244. expect(response.statusCode).to.equal(200)
  245. let updatedProject = await owner.getProject(projectOneId)
  246. let file = updatedProject.rootFolder[0].fileRefs[0]
  247. expect(file.linkedFileData).to.deep.equal({
  248. provider: 'url',
  249. url: 'http://example.com/foo',
  250. importedAt: new Date().toISOString(),
  251. })
  252. ;({ response, body } = await owner.doRequest(
  253. 'get',
  254. `/project/${projectOneId}/file/${file._id}`
  255. ))
  256. expect(response.statusCode).to.equal(200)
  257. expect(body).to.equal('foo foo foo')
  258. // replacing the file
  259. ;({ response, body } = await owner.doRequest('post', {
  260. url: `/project/${projectOneId}/linked_file`,
  261. json: {
  262. provider: 'url',
  263. data: {
  264. url: 'http://example.com/foo',
  265. },
  266. parent_folder_id: projectOneRootFolderId,
  267. name: 'url-test-file-2',
  268. },
  269. }))
  270. expect(response.statusCode).to.equal(200)
  271. ;({ response, body } = await owner.doRequest('post', {
  272. url: `/project/${projectOneId}/linked_file`,
  273. json: {
  274. provider: 'url',
  275. data: {
  276. url: 'http://example.com/bar',
  277. },
  278. parent_folder_id: projectOneRootFolderId,
  279. name: 'url-test-file-2',
  280. },
  281. }))
  282. expect(response.statusCode).to.equal(200)
  283. updatedProject = await owner.getProject(projectOneId)
  284. file = updatedProject.rootFolder[0].fileRefs[1]
  285. expect(file.linkedFileData).to.deep.equal({
  286. provider: 'url',
  287. url: 'http://example.com/bar',
  288. importedAt: new Date().toISOString(),
  289. })
  290. ;({ response, body } = await owner.doRequest(
  291. 'get',
  292. `/project/${projectOneId}/file/${file._id}`
  293. ))
  294. expect(response.statusCode).to.equal(200)
  295. expect(body).to.equal('bar bar bar')
  296. })
  297. it('should return an error if the file exceeds the maximum size', async function () {
  298. // download does not succeed
  299. const { response, body } = await owner.doRequest('post', {
  300. url: `/project/${projectOneId}/linked_file`,
  301. json: {
  302. provider: 'url',
  303. data: {
  304. url: 'http://example.com/large',
  305. },
  306. parent_folder_id: projectOneRootFolderId,
  307. name: 'url-large-file-1',
  308. },
  309. })
  310. expect(response.statusCode).to.equal(422)
  311. expect(body).to.equal('File too large')
  312. })
  313. it("should return an error if the file can't be downloaded", async function () {
  314. // download does not succeed
  315. let { response, body } = await owner.doRequest('post', {
  316. url: `/project/${projectOneId}/linked_file`,
  317. json: {
  318. provider: 'url',
  319. data: {
  320. url: 'http://example.com/does-not-exist',
  321. },
  322. parent_folder_id: projectOneRootFolderId,
  323. name: 'url-test-file-3',
  324. },
  325. })
  326. expect(response.statusCode).to.equal(422) // unprocessable
  327. expect(body).to.equal(
  328. 'Your URL could not be reached (404 status code). Please check it and try again.'
  329. )
  330. // url is invalid
  331. ;({ response, body } = await owner.doRequest('post', {
  332. url: `/project/${projectOneId}/linked_file`,
  333. json: {
  334. provider: 'url',
  335. data: {
  336. url: '!^$%',
  337. },
  338. parent_folder_id: projectOneRootFolderId,
  339. name: 'url-test-file-4',
  340. },
  341. }))
  342. expect(response.statusCode).to.equal(422) // unprocessable
  343. expect(body).to.equal(
  344. 'Your URL is not valid. Please check it and try again.'
  345. )
  346. // URL is non-http
  347. ;({ response, body } = await owner.doRequest('post', {
  348. url: `/project/${projectOneId}/linked_file`,
  349. json: {
  350. provider: 'url',
  351. data: {
  352. url: 'ftp://127.0.0.1',
  353. },
  354. parent_folder_id: projectOneRootFolderId,
  355. name: 'url-test-file-5',
  356. },
  357. }))
  358. expect(response.statusCode).to.equal(422) // unprocessable
  359. expect(body).to.equal(
  360. 'Your URL is not valid. Please check it and try again.'
  361. )
  362. })
  363. it('should accept a URL withuot a leading http://, and add it', async function () {
  364. let { response, body } = await owner.doRequest('post', {
  365. url: `/project/${projectOneId}/linked_file`,
  366. json: {
  367. provider: 'url',
  368. data: {
  369. url: 'example.com/foo',
  370. },
  371. parent_folder_id: projectOneRootFolderId,
  372. name: 'url-test-file-6',
  373. },
  374. })
  375. expect(response.statusCode).to.equal(200)
  376. const updatedProject = await owner.getProject(projectOneId)
  377. const file = _.find(
  378. updatedProject.rootFolder[0].fileRefs,
  379. file => file.name === 'url-test-file-6'
  380. )
  381. expect(file.linkedFileData).to.deep.equal({
  382. provider: 'url',
  383. url: 'http://example.com/foo',
  384. importedAt: new Date().toISOString(),
  385. })
  386. ;({ response, body } = await owner.doRequest(
  387. 'get',
  388. `/project/${projectOneId}/file/${file._id}`
  389. ))
  390. expect(response.statusCode).to.equal(200)
  391. expect(body).to.equal('foo foo foo')
  392. })
  393. })
  394. // TODO: Add test for asking for host that return ENOTFOUND
  395. // (This will probably end up handled by the proxy)
  396. describe('creating a linked output file', function () {
  397. beforeEach(async function () {
  398. projectOneId = await owner.createProject('output-test-one', {
  399. template: 'blank',
  400. })
  401. projectOne = await owner.getProject(projectOneId)
  402. projectOneRootFolderId = projectOne.rootFolder[0]._id.toString()
  403. projectTwoId = await owner.createProject('output-test-two', {
  404. template: 'blank',
  405. })
  406. projectTwo = await owner.getProject(projectTwoId)
  407. projectTwoRootFolderId = projectTwo.rootFolder[0]._id.toString()
  408. })
  409. it('should import the project.pdf file from the source project and refresh it', async function () {
  410. // import the file
  411. let { response, body } = await owner.doRequest('post', {
  412. url: `/project/${projectOneId}/linked_file`,
  413. json: {
  414. name: 'test.pdf',
  415. parent_folder_id: projectOneRootFolderId,
  416. provider: 'project_output_file',
  417. data: {
  418. source_project_id: projectTwoId,
  419. source_output_file_path: 'project.pdf',
  420. build_id: '1234-abcd',
  421. },
  422. },
  423. })
  424. expect(response.statusCode).to.equal(200)
  425. const existingFileId = body.new_file_id
  426. expect(existingFileId).to.exist
  427. const updatedProject = await owner.getProject(projectOneId)
  428. const firstFile = updatedProject.rootFolder[0].fileRefs[0]
  429. expect(firstFile._id.toString()).to.equal(existingFileId.toString())
  430. expect(firstFile.linkedFileData).to.deep.equal({
  431. provider: 'project_output_file',
  432. source_project_id: projectTwoId,
  433. source_output_file_path: 'project.pdf',
  434. build_id: '1234-abcd',
  435. importedAt: new Date().toISOString(),
  436. })
  437. expect(firstFile.name).to.equal('test.pdf')
  438. // refresh the file
  439. ;({ response, body } = await owner.doRequest('post', {
  440. url: `/project/${projectOneId}/linked_file/${existingFileId}/refresh`,
  441. json: true,
  442. }))
  443. expect(response.statusCode).to.equal(200)
  444. const refreshedFileId = body.new_file_id
  445. expect(refreshedFileId).to.exist
  446. expect(refreshedFileId).to.not.equal(existingFileId)
  447. const refreshedProject = await owner.getProject(projectOneId)
  448. const refreshedFile = refreshedProject.rootFolder[0].fileRefs[0]
  449. expect(refreshedFile._id.toString()).to.equal(refreshedFileId.toString())
  450. expect(refreshedFile.name).to.equal('test.pdf')
  451. })
  452. })
  453. describe('with a linked project_output_file from a v1 project that has not been imported', function () {
  454. beforeEach(async function () {
  455. projectOneId = await owner.createProject('output-v1-test-one', {
  456. template: 'blank',
  457. })
  458. projectOne = await owner.getProject(projectOneId)
  459. projectOneRootFolderId = projectOne.rootFolder[0]._id.toString()
  460. projectOne.rootFolder[0].fileRefs.push({
  461. linkedFileData: {
  462. provider: 'project_output_file',
  463. v1_source_doc_id: 9999999, // We won't find this id in the database
  464. source_output_file_path: 'project.pdf',
  465. },
  466. _id: 'abcdef',
  467. rev: 0,
  468. created: new Date(),
  469. name: 'whatever.pdf',
  470. })
  471. await owner.saveProject(projectOne)
  472. })
  473. it('should refuse to refresh', async function () {
  474. const { response, body } = await owner.doRequest('post', {
  475. url: `/project/${projectOneId}/linked_file/abcdef/refresh`,
  476. json: true,
  477. })
  478. expect(response.statusCode).to.equal(409)
  479. expect(body).to.equal(
  480. 'Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file'
  481. )
  482. })
  483. })
  484. })