LinkedFilesTests.js 16 KB

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