UpdateMerger.test.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. import { vi, expect } from 'vitest'
  2. import sinon from 'sinon'
  3. import { Writable } from 'stream'
  4. import mongodb from 'mongodb-legacy'
  5. const { ObjectId } = mongodb
  6. const MODULE_PATH =
  7. '../../../../app/src/Features/ThirdPartyDataStore/UpdateMerger.mjs'
  8. describe('UpdateMerger :', function () {
  9. beforeEach(async function (ctx) {
  10. ctx.projectId = 'project_id_here'
  11. ctx.userId = 'mock-user-id'
  12. ctx.randomUUID = 'random-uuid'
  13. ctx.dumpPath = '/dump'
  14. ctx.docPath = ctx.newDocPath = '/folder/doc.tex'
  15. ctx.filePath = ctx.newFilePath = '/folder/file.png'
  16. ctx.existingDocPath = '/folder/other.tex'
  17. ctx.existingFilePath = '/folder/fig1.pdf'
  18. ctx.linkedFileData = { provider: 'url' }
  19. ctx.existingDocs = [{ path: '/main.tex' }, { path: '/folder/other.tex' }]
  20. ctx.existingFiles = [{ path: '/figure.pdf' }, { path: '/folder/fig1.pdf' }]
  21. ctx.fsPath = `${ctx.dumpPath}/${ctx.projectId}_${ctx.randomUUID}`
  22. ctx.fileContents = `\\documentclass{article}
  23. \\usepackage[utf8]{inputenc}
  24. \\title{42}
  25. \\author{Jane Doe}
  26. \\date{June 2011}`
  27. ctx.docLines = ctx.fileContents.split('\n')
  28. ctx.source = 'dropbox'
  29. ctx.updateRequest = new Writable()
  30. ctx.writeStream = new Writable()
  31. ctx.fsPromises = {
  32. unlink: sinon.stub().resolves(),
  33. readFile: sinon.stub().withArgs(ctx.fsPath).resolves(ctx.fileContents),
  34. mkdir: sinon.stub().resolves(),
  35. }
  36. ctx.fs = {
  37. createWriteStream: sinon.stub().returns(ctx.writeStream),
  38. }
  39. ctx.doc = {
  40. _id: new ObjectId(),
  41. rev: 2,
  42. }
  43. ctx.file = {
  44. _id: new ObjectId(),
  45. rev: 6,
  46. }
  47. ctx.folder = {
  48. _id: new ObjectId(),
  49. }
  50. ctx.EditorController = {
  51. promises: {
  52. deleteEntityWithPath: sinon.stub().resolves(new ObjectId()),
  53. upsertDocWithPath: sinon
  54. .stub()
  55. .resolves({ doc: ctx.doc, folder: ctx.folder }),
  56. upsertFileWithPath: sinon
  57. .stub()
  58. .resolves({ file: ctx.file, folder: ctx.folder }),
  59. },
  60. }
  61. ctx.FileTypeManager = {
  62. promises: {
  63. getType: sinon.stub(),
  64. },
  65. }
  66. ctx.crypto = {
  67. randomUUID: sinon.stub().returns(ctx.randomUUID),
  68. }
  69. ctx.ProjectEntityHandler = {
  70. promises: {
  71. getAllEntities: sinon.stub().resolves({
  72. docs: ctx.existingDocs,
  73. files: ctx.existingFiles,
  74. }),
  75. },
  76. }
  77. ctx.Settings = { path: { dumpFolder: ctx.dumpPath } }
  78. ctx.stream = { pipeline: sinon.stub().resolves() }
  79. vi.doMock('fs/promises', () => ({
  80. default: ctx.fsPromises,
  81. }))
  82. vi.doMock('fs', () => ({
  83. default: ctx.fs,
  84. }))
  85. vi.doMock('../../../../app/src/Features/Editor/EditorController', () => ({
  86. default: ctx.EditorController,
  87. }))
  88. vi.doMock('../../../../app/src/Features/Uploads/FileTypeManager', () => ({
  89. default: ctx.FileTypeManager,
  90. }))
  91. vi.doMock(
  92. '../../../../app/src/Features/Project/ProjectEntityHandler',
  93. () => ({
  94. default: ctx.ProjectEntityHandler,
  95. })
  96. )
  97. vi.doMock('@overleaf/settings', () => ({
  98. default: ctx.Settings,
  99. }))
  100. vi.doMock('stream/promises', () => ({
  101. pipeline: ctx.stream.pipeline,
  102. }))
  103. vi.doMock('crypto', () => ({
  104. default: ctx.crypto,
  105. }))
  106. ctx.UpdateMerger = (await import(MODULE_PATH)).default
  107. })
  108. describe('mergeUpdate', function () {
  109. describe('doc updates for a new doc', function () {
  110. beforeEach(async function (ctx) {
  111. ctx.FileTypeManager.promises.getType.resolves({
  112. binary: false,
  113. encoding: 'utf-8',
  114. })
  115. ctx.mergeUpdateResult = await ctx.UpdateMerger.promises.mergeUpdate(
  116. ctx.userId,
  117. ctx.projectId,
  118. ctx.docPath,
  119. ctx.updateRequest,
  120. ctx.source
  121. )
  122. })
  123. it('should look at the file contents', function (ctx) {
  124. expect(ctx.FileTypeManager.promises.getType).to.have.been.called
  125. })
  126. it('should process update as doc', function (ctx) {
  127. expect(
  128. ctx.EditorController.promises.upsertDocWithPath
  129. ).to.have.been.calledWith(
  130. ctx.projectId,
  131. ctx.docPath,
  132. ctx.docLines,
  133. ctx.source,
  134. ctx.userId
  135. )
  136. })
  137. it('removes the temp file from disk', function (ctx) {
  138. expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.fsPath)
  139. })
  140. it('returns the entity id and rev', function (ctx) {
  141. expect(ctx.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
  142. expect(ctx.mergeUpdateResult.rev).to.equal(2)
  143. })
  144. })
  145. describe('file updates for a new file ', function () {
  146. beforeEach(async function (ctx) {
  147. ctx.FileTypeManager.promises.getType.resolves({ binary: true })
  148. ctx.mergeUpdateResult = await ctx.UpdateMerger.promises.mergeUpdate(
  149. ctx.userId,
  150. ctx.projectId,
  151. ctx.filePath,
  152. ctx.updateRequest,
  153. ctx.source
  154. )
  155. })
  156. it('should look at the file contents', function (ctx) {
  157. expect(ctx.FileTypeManager.promises.getType).to.have.been.called
  158. })
  159. it('should process update as file', function (ctx) {
  160. expect(
  161. ctx.EditorController.promises.upsertFileWithPath
  162. ).to.have.been.calledWith(
  163. ctx.projectId,
  164. ctx.filePath,
  165. ctx.fsPath,
  166. null,
  167. ctx.source,
  168. ctx.userId
  169. )
  170. })
  171. it('removes the temp file from disk', function (ctx) {
  172. expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.fsPath)
  173. })
  174. it('returns the entity id and rev', function (ctx) {
  175. expect(ctx.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
  176. expect(ctx.mergeUpdateResult.rev).to.equal(6)
  177. })
  178. })
  179. describe('doc updates for an existing doc', function () {
  180. beforeEach(async function (ctx) {
  181. ctx.FileTypeManager.promises.getType.resolves({
  182. binary: false,
  183. encoding: 'utf-8',
  184. })
  185. ctx.mergeUpdateResult = await ctx.UpdateMerger.promises.mergeUpdate(
  186. ctx.userId,
  187. ctx.projectId,
  188. ctx.existingDocPath,
  189. ctx.updateRequest,
  190. ctx.source
  191. )
  192. })
  193. it('should look at the file contents', function (ctx) {
  194. expect(ctx.FileTypeManager.promises.getType).to.have.been.called
  195. })
  196. it('should process update as doc', function (ctx) {
  197. expect(
  198. ctx.EditorController.promises.upsertDocWithPath
  199. ).to.have.been.calledWith(
  200. ctx.projectId,
  201. ctx.existingDocPath,
  202. ctx.docLines,
  203. ctx.source,
  204. ctx.userId
  205. )
  206. })
  207. it('removes the temp file from disk', function (ctx) {
  208. expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.fsPath)
  209. })
  210. it('returns the entity id and rev', function (ctx) {
  211. expect(ctx.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
  212. expect(ctx.mergeUpdateResult.rev).to.equal(2)
  213. })
  214. })
  215. describe('file updates for an existing file', function () {
  216. beforeEach(async function (ctx) {
  217. ctx.FileTypeManager.promises.getType.resolves({ binary: true })
  218. ctx.mergeUpdateResult = await ctx.UpdateMerger.promises.mergeUpdate(
  219. ctx.userId,
  220. ctx.projectId,
  221. ctx.existingFilePath,
  222. ctx.updateRequest,
  223. ctx.source
  224. )
  225. })
  226. it('should look at the file contents', function (ctx) {
  227. expect(ctx.FileTypeManager.promises.getType).to.have.been.called
  228. })
  229. it('should process update as file', function (ctx) {
  230. expect(
  231. ctx.EditorController.promises.upsertFileWithPath
  232. ).to.have.been.calledWith(
  233. ctx.projectId,
  234. ctx.existingFilePath,
  235. ctx.fsPath,
  236. null,
  237. ctx.source,
  238. ctx.userId
  239. )
  240. })
  241. it('removes the temp file from disk', function (ctx) {
  242. expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.fsPath)
  243. })
  244. it('returns the entity id and rev', function (ctx) {
  245. expect(ctx.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
  246. expect(ctx.mergeUpdateResult.rev).to.equal(6)
  247. })
  248. })
  249. })
  250. describe('file updates for an existing doc', function () {
  251. beforeEach(async function (ctx) {
  252. ctx.FileTypeManager.promises.getType.resolves({ binary: true })
  253. ctx.mergeUpdateResult = await ctx.UpdateMerger.promises.mergeUpdate(
  254. ctx.userId,
  255. ctx.projectId,
  256. ctx.existingDocPath,
  257. ctx.updateRequest,
  258. ctx.source
  259. )
  260. })
  261. it('should look at the file contents', function (ctx) {
  262. expect(ctx.FileTypeManager.promises.getType).to.have.been.called
  263. })
  264. it('should process update as file', function (ctx) {
  265. expect(
  266. ctx.EditorController.promises.upsertFileWithPath
  267. ).to.have.been.calledWith(
  268. ctx.projectId,
  269. ctx.existingDocPath,
  270. ctx.fsPath,
  271. null,
  272. ctx.source,
  273. ctx.userId
  274. )
  275. })
  276. it('removes the temp file from disk', function (ctx) {
  277. expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.fsPath)
  278. })
  279. it('returns the entity id and rev', function (ctx) {
  280. expect(ctx.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
  281. expect(ctx.mergeUpdateResult.rev).to.equal(6)
  282. })
  283. })
  284. describe('doc updates for an existing file', function () {
  285. beforeEach(async function (ctx) {
  286. ctx.FileTypeManager.promises.getType.resolves({ binary: true })
  287. ctx.mergeUpdateResult = await ctx.UpdateMerger.promises.mergeUpdate(
  288. ctx.userId,
  289. ctx.projectId,
  290. ctx.existingFilePath,
  291. ctx.updateRequest,
  292. ctx.source
  293. )
  294. })
  295. it('should look at the file contents', function (ctx) {
  296. expect(ctx.FileTypeManager.promises.getType).to.have.been.called
  297. })
  298. it('should not delete the existing file', function (ctx) {
  299. expect(ctx.EditorController.promises.deleteEntityWithPath).to.not.have
  300. .been.called
  301. })
  302. it('should process update as file', function (ctx) {
  303. expect(
  304. ctx.EditorController.promises.upsertFileWithPath
  305. ).to.have.been.calledWith(
  306. ctx.projectId,
  307. ctx.existingFilePath,
  308. ctx.fsPath,
  309. null,
  310. ctx.source,
  311. ctx.userId
  312. )
  313. })
  314. it('removes the temp file from disk', function (ctx) {
  315. expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.fsPath)
  316. })
  317. it('returns the entity id and rev', function (ctx) {
  318. expect(ctx.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
  319. expect(ctx.mergeUpdateResult.rev).to.equal(6)
  320. })
  321. })
  322. describe('deleteUpdate', function () {
  323. beforeEach(async function (ctx) {
  324. ctx.deleteUpdateResult = await ctx.UpdateMerger.promises.deleteUpdate(
  325. ctx.userId,
  326. ctx.projectId,
  327. ctx.docPath,
  328. ctx.source
  329. )
  330. })
  331. afterEach(function (ctx) {
  332. delete ctx.deleteUpdateResult
  333. })
  334. it('should delete the entity in the editor controller', function (ctx) {
  335. expect(
  336. ctx.EditorController.promises.deleteEntityWithPath
  337. ).to.have.been.calledWith(
  338. ctx.projectId,
  339. ctx.docPath,
  340. ctx.source,
  341. ctx.userId
  342. )
  343. })
  344. it('returns the entity id', function (ctx) {
  345. expect(ctx.deleteUpdateResult).to.be.instanceOf(ObjectId)
  346. })
  347. })
  348. })