LinkedFilesTests.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  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('You do not have access to this project')
  144. })
  145. it('should generate a proper error message when the source file has been deleted', async function () {
  146. // import the file from the source project
  147. let { response, body } = await owner.doRequest('post', {
  148. url: `/project/${projectOneId}/linked_file`,
  149. json: {
  150. name: 'test-link.txt',
  151. parent_folder_id: projectOneRootFolderId,
  152. provider: 'project_file',
  153. data: {
  154. source_project_id: projectTwoId,
  155. source_entity_path: `/${sourceDocName}`,
  156. },
  157. },
  158. })
  159. expect(response.statusCode).to.equal(200)
  160. const existingFileId = body.new_file_id
  161. expect(existingFileId).to.exist
  162. // rename the source file
  163. await owner.renameItemInProject(
  164. projectTwoId,
  165. 'doc',
  166. sourceDocId,
  167. 'renamed-doc.txt'
  168. )
  169. // refresh the file
  170. ;({ response, body } = await owner.doRequest('post', {
  171. url: `/project/${projectOneId}/linked_file/${existingFileId}/refresh`,
  172. json: true,
  173. }))
  174. expect(response.statusCode).to.equal(404)
  175. expect(body).to.equal('Source file not found')
  176. })
  177. })
  178. describe('with a linked project_file from a v1 project that has not been imported', function () {
  179. beforeEach(async function () {
  180. projectOneId = await owner.createProject('plf-v1-test-one', {
  181. template: 'blank',
  182. })
  183. projectOne = await owner.getProject(projectOneId)
  184. projectOneRootFolderId = projectOne.rootFolder[0]._id.toString()
  185. projectOne.rootFolder[0].fileRefs.push({
  186. linkedFileData: {
  187. provider: 'project_file',
  188. v1_source_doc_id: 9999999, // We won't find this id in the database
  189. source_entity_path: 'example.jpeg',
  190. },
  191. _id: 'abcd',
  192. rev: 0,
  193. created: new Date(),
  194. name: 'example.jpeg',
  195. })
  196. await owner.saveProject(projectOne)
  197. })
  198. it('should refuse to refresh', async function () {
  199. const { response, body } = await owner.doRequest('post', {
  200. url: `/project/${projectOneId}/linked_file/abcd/refresh`,
  201. json: true,
  202. })
  203. expect(response.statusCode).to.equal(409)
  204. expect(body).to.equal(
  205. 'Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file'
  206. )
  207. })
  208. })
  209. describe('creating a URL based linked file', function () {
  210. beforeEach(async function () {
  211. projectOneId = await owner.createProject('url-linked-files-project', {
  212. template: 'blank',
  213. })
  214. projectOne = await owner.getProject(projectOneId)
  215. projectOneRootFolderId = projectOne.rootFolder[0]._id.toString()
  216. })
  217. it('should download, create and replace a file', async function () {
  218. // downloading the initial file
  219. let { response, body } = await owner.doRequest('post', {
  220. url: `/project/${projectOneId}/linked_file`,
  221. json: {
  222. provider: 'url',
  223. data: {
  224. url: 'http://example.com/foo',
  225. },
  226. parent_folder_id: projectOneRootFolderId,
  227. name: 'url-test-file-1',
  228. },
  229. })
  230. expect(response.statusCode).to.equal(200)
  231. let updatedProject = await owner.getProject(projectOneId)
  232. let file = updatedProject.rootFolder[0].fileRefs[0]
  233. expect(file.linkedFileData).to.deep.equal({
  234. provider: 'url',
  235. url: 'http://example.com/foo',
  236. })
  237. ;({ response, body } = await owner.doRequest(
  238. 'get',
  239. `/project/${projectOneId}/file/${file._id}`
  240. ))
  241. expect(response.statusCode).to.equal(200)
  242. expect(body).to.equal('foo foo foo')
  243. // replacing the file
  244. ;({ response, body } = await owner.doRequest('post', {
  245. url: `/project/${projectOneId}/linked_file`,
  246. json: {
  247. provider: 'url',
  248. data: {
  249. url: 'http://example.com/foo',
  250. },
  251. parent_folder_id: projectOneRootFolderId,
  252. name: 'url-test-file-2',
  253. },
  254. }))
  255. expect(response.statusCode).to.equal(200)
  256. ;({ response, body } = await owner.doRequest('post', {
  257. url: `/project/${projectOneId}/linked_file`,
  258. json: {
  259. provider: 'url',
  260. data: {
  261. url: 'http://example.com/bar',
  262. },
  263. parent_folder_id: projectOneRootFolderId,
  264. name: 'url-test-file-2',
  265. },
  266. }))
  267. expect(response.statusCode).to.equal(200)
  268. updatedProject = await owner.getProject(projectOneId)
  269. file = updatedProject.rootFolder[0].fileRefs[1]
  270. expect(file.linkedFileData).to.deep.equal({
  271. provider: 'url',
  272. url: 'http://example.com/bar',
  273. })
  274. ;({ response, body } = await owner.doRequest(
  275. 'get',
  276. `/project/${projectOneId}/file/${file._id}`
  277. ))
  278. expect(response.statusCode).to.equal(200)
  279. expect(body).to.equal('bar bar bar')
  280. })
  281. it("should return an error if the file can't be downloaded", async function () {
  282. // download does not succeed
  283. let { response, body } = await owner.doRequest('post', {
  284. url: `/project/${projectOneId}/linked_file`,
  285. json: {
  286. provider: 'url',
  287. data: {
  288. url: 'http://example.com/does-not-exist',
  289. },
  290. parent_folder_id: projectOneRootFolderId,
  291. name: 'url-test-file-3',
  292. },
  293. })
  294. expect(response.statusCode).to.equal(422) // unprocessable
  295. expect(body).to.equal(
  296. 'Your URL could not be reached (404 status code). Please check it and try again.'
  297. )
  298. // url is invalid
  299. ;({ response, body } = await owner.doRequest('post', {
  300. url: `/project/${projectOneId}/linked_file`,
  301. json: {
  302. provider: 'url',
  303. data: {
  304. url: '!^$%',
  305. },
  306. parent_folder_id: projectOneRootFolderId,
  307. name: 'url-test-file-4',
  308. },
  309. }))
  310. expect(response.statusCode).to.equal(422) // unprocessable
  311. expect(body).to.equal(
  312. 'Your URL is not valid. Please check it and try again.'
  313. )
  314. // URL is non-http
  315. ;({ response, body } = await owner.doRequest('post', {
  316. url: `/project/${projectOneId}/linked_file`,
  317. json: {
  318. provider: 'url',
  319. data: {
  320. url: 'ftp://localhost',
  321. },
  322. parent_folder_id: projectOneRootFolderId,
  323. name: 'url-test-file-5',
  324. },
  325. }))
  326. expect(response.statusCode).to.equal(422) // unprocessable
  327. expect(body).to.equal(
  328. 'Your URL is not valid. Please check it and try again.'
  329. )
  330. })
  331. it('should accept a URL withuot a leading http://, and add it', async function () {
  332. let { response, body } = await owner.doRequest('post', {
  333. url: `/project/${projectOneId}/linked_file`,
  334. json: {
  335. provider: 'url',
  336. data: {
  337. url: 'example.com/foo',
  338. },
  339. parent_folder_id: projectOneRootFolderId,
  340. name: 'url-test-file-6',
  341. },
  342. })
  343. expect(response.statusCode).to.equal(200)
  344. const updatedProject = await owner.getProject(projectOneId)
  345. const file = _.find(
  346. updatedProject.rootFolder[0].fileRefs,
  347. file => file.name === 'url-test-file-6'
  348. )
  349. expect(file.linkedFileData).to.deep.equal({
  350. provider: 'url',
  351. url: 'http://example.com/foo',
  352. })
  353. ;({ response, body } = await owner.doRequest(
  354. 'get',
  355. `/project/${projectOneId}/file/${file._id}`
  356. ))
  357. expect(response.statusCode).to.equal(200)
  358. expect(body).to.equal('foo foo foo')
  359. })
  360. })
  361. // TODO: Add test for asking for host that return ENOTFOUND
  362. // (This will probably end up handled by the proxy)
  363. describe('creating a linked output file', function () {
  364. beforeEach(async function () {
  365. projectOneId = await owner.createProject('output-test-one', {
  366. template: 'blank',
  367. })
  368. projectOne = await owner.getProject(projectOneId)
  369. projectOneRootFolderId = projectOne.rootFolder[0]._id.toString()
  370. projectTwoId = await owner.createProject('output-test-two', {
  371. template: 'blank',
  372. })
  373. projectTwo = await owner.getProject(projectTwoId)
  374. projectTwoRootFolderId = projectTwo.rootFolder[0]._id.toString()
  375. })
  376. it('should import the project.pdf file from the source project and refresh it', async function () {
  377. // import the file
  378. let { response, body } = await owner.doRequest('post', {
  379. url: `/project/${projectOneId}/linked_file`,
  380. json: {
  381. name: 'test.pdf',
  382. parent_folder_id: projectOneRootFolderId,
  383. provider: 'project_output_file',
  384. data: {
  385. source_project_id: projectTwoId,
  386. source_output_file_path: 'project.pdf',
  387. build_id: '1234-abcd',
  388. },
  389. },
  390. })
  391. expect(response.statusCode).to.equal(200)
  392. const existingFileId = body.new_file_id
  393. expect(existingFileId).to.exist
  394. const updatedProject = await owner.getProject(projectOneId)
  395. const firstFile = updatedProject.rootFolder[0].fileRefs[0]
  396. expect(firstFile._id.toString()).to.equal(existingFileId.toString())
  397. expect(firstFile.linkedFileData).to.deep.equal({
  398. provider: 'project_output_file',
  399. source_project_id: projectTwoId,
  400. source_output_file_path: 'project.pdf',
  401. build_id: '1234-abcd',
  402. })
  403. expect(firstFile.name).to.equal('test.pdf')
  404. // refresh the file
  405. ;({ response, body } = await owner.doRequest('post', {
  406. url: `/project/${projectOneId}/linked_file/${existingFileId}/refresh`,
  407. json: true,
  408. }))
  409. expect(response.statusCode).to.equal(200)
  410. const refreshedFileId = body.new_file_id
  411. expect(refreshedFileId).to.exist
  412. expect(refreshedFileId).to.not.equal(existingFileId)
  413. const refreshedProject = await owner.getProject(projectOneId)
  414. const refreshedFile = refreshedProject.rootFolder[0].fileRefs[0]
  415. expect(refreshedFile._id.toString()).to.equal(refreshedFileId.toString())
  416. expect(refreshedFile.name).to.equal('test.pdf')
  417. })
  418. })
  419. describe('with a linked project_output_file from a v1 project that has not been imported', function () {
  420. beforeEach(async function () {
  421. projectOneId = await owner.createProject('output-v1-test-one', {
  422. template: 'blank',
  423. })
  424. projectOne = await owner.getProject(projectOneId)
  425. projectOneRootFolderId = projectOne.rootFolder[0]._id.toString()
  426. projectOne.rootFolder[0].fileRefs.push({
  427. linkedFileData: {
  428. provider: 'project_output_file',
  429. v1_source_doc_id: 9999999, // We won't find this id in the database
  430. source_output_file_path: 'project.pdf',
  431. },
  432. _id: 'abcdef',
  433. rev: 0,
  434. created: new Date(),
  435. name: 'whatever.pdf',
  436. })
  437. await owner.saveProject(projectOne)
  438. })
  439. it('should refuse to refresh', async function () {
  440. const { response, body } = await owner.doRequest('post', {
  441. url: `/project/${projectOneId}/linked_file/abcdef/refresh`,
  442. json: true,
  443. })
  444. expect(response.statusCode).to.equal(409)
  445. expect(body).to.equal(
  446. 'Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file'
  447. )
  448. })
  449. })
  450. })