LinkedFilesTests.js 16 KB

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