ProjectHistoryControllerTests.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. const sinon = require('sinon')
  2. const nock = require('nock')
  3. const { expect } = require('chai')
  4. const fs = require('fs')
  5. const path = require('path')
  6. const SandboxedModule = require('sandboxed-module')
  7. const { ObjectId } = require('mongodb')
  8. const unzipper = require('unzipper')
  9. const modulePath = '../../../app/src/ProjectHistoryController'
  10. describe('ProjectHistoryController', function () {
  11. const projectId = ObjectId('611bd20c5d76a3c1bd0c7c13')
  12. const deletedFileId = ObjectId('60f6e92c6c14d84fb7a71ae1')
  13. const historyId = 123
  14. let clock
  15. const now = new Date(Date.UTC(2021, 1, 1, 0, 0)).getTime()
  16. before(async function () {
  17. clock = sinon.useFakeTimers({
  18. now,
  19. shouldAdvanceTime: true,
  20. })
  21. })
  22. after(function () {
  23. // clock.runAll()
  24. clock.restore()
  25. })
  26. beforeEach(function () {
  27. this.db = {
  28. users: {
  29. countDocuments: sinon.stub().yields(),
  30. },
  31. }
  32. this.project = {
  33. _id: ObjectId('611bd20c5d76a3c1bd0c7c13'),
  34. name: 'My Test Project',
  35. rootDoc_id: ObjectId('611bd20c5d76a3c1bd0c7c15'),
  36. rootFolder: [
  37. {
  38. _id: ObjectId('611bd20c5d76a3c1bd0c7c12'),
  39. name: 'rootFolder',
  40. folders: [
  41. {
  42. _id: ObjectId('611bd242e64281c13303d6b5'),
  43. name: 'a folder',
  44. folders: [
  45. {
  46. _id: ObjectId('611bd247e64281c13303d6b7'),
  47. name: 'a subfolder',
  48. folders: [],
  49. fileRefs: [],
  50. docs: [
  51. {
  52. _id: ObjectId('611bd24ee64281c13303d6b9'),
  53. name: 'a renamed file in a subfolder.tex',
  54. },
  55. ],
  56. },
  57. ],
  58. fileRefs: [],
  59. docs: [],
  60. },
  61. {
  62. _id: ObjectId('611bd34ee64281c13303d6be'),
  63. name: 'images',
  64. folders: [],
  65. fileRefs: [
  66. {
  67. _id: ObjectId('611bd2bce64281c13303d6bb'),
  68. name: 'overleaf-white.svg',
  69. linkedFileData: {
  70. provider: 'url',
  71. url: 'https://cdn.overleaf.com/img/ol-brand/overleaf-white.svg',
  72. },
  73. created: '2021-08-17T15:16:12.753Z',
  74. },
  75. ],
  76. docs: [],
  77. },
  78. ],
  79. fileRefs: [
  80. {
  81. _id: ObjectId('611bd20c5d76a3c1bd0c7c19'),
  82. name: 'universe.jpg',
  83. linkedFileData: null,
  84. created: '2021-08-17T15:13:16.400Z',
  85. },
  86. ],
  87. docs: [
  88. {
  89. _id: ObjectId('611bd20c5d76a3c1bd0c7c15'),
  90. name: 'main.tex',
  91. },
  92. {
  93. _id: ObjectId('611bd20c5d76a3c1bd0c7c17'),
  94. name: 'references.bib',
  95. },
  96. ],
  97. },
  98. ],
  99. compiler: 'pdflatex',
  100. description: '',
  101. deletedDocs: [],
  102. members: [],
  103. invites: [],
  104. owner: {
  105. _id: ObjectId('611572e24bff88527f61dccd'),
  106. first_name: 'Test',
  107. last_name: 'User',
  108. email: 'test@example.com',
  109. privileges: 'owner',
  110. signUpDate: '2021-08-12T19:13:38.462Z',
  111. },
  112. features: {},
  113. }
  114. this.multi = {
  115. del: sinon.stub(),
  116. rpush: sinon.stub(),
  117. exec: sinon.stub().yields(null, 1),
  118. }
  119. const { docs, folders } = this.project.rootFolder[0]
  120. const allDocs = [...docs]
  121. const processFolders = folders => {
  122. for (const folder of folders) {
  123. for (const doc of folder.docs) {
  124. allDocs.push(doc)
  125. }
  126. if (folder.folders) {
  127. processFolders(folder.folders)
  128. }
  129. }
  130. }
  131. processFolders(folders)
  132. allDocs.forEach(doc => {
  133. doc.lines = [`this is the contents of ${doc.name}`]
  134. })
  135. // handle Doc.find().lean().cursor()
  136. this.findDocs = sinon.stub().returns({
  137. lean: sinon.stub().returns({
  138. cursor: sinon.stub().returns(allDocs),
  139. }),
  140. })
  141. // handle await Doc.findOne().lean() - single result, no cursor required
  142. this.findOneDoc = sinon.stub().callsFake(id => {
  143. const result = allDocs.find(doc => {
  144. return doc._id.toString() === id.toString()
  145. })
  146. return { lean: sinon.stub().resolves(result) }
  147. })
  148. this.deletedFiles = [
  149. {
  150. _id: deletedFileId,
  151. name: 'testing.tex',
  152. deletedAt: new Date(),
  153. },
  154. ]
  155. // handle DeletedFile.find().lean().cursor()
  156. this.findDeletedFiles = sinon.stub().returns({
  157. lean: sinon
  158. .stub()
  159. .returns({ cursor: sinon.stub().returns(this.deletedFiles) }),
  160. })
  161. this.ProjectGetter = {
  162. promises: {
  163. getProject: sinon.stub().resolves(this.project),
  164. },
  165. }
  166. this.FileStoreHandler = {
  167. _buildUrl: (projectId, fileId) =>
  168. `http://filestore.test/${projectId}/${fileId}`,
  169. }
  170. this.ProjectHistoryHandler = {
  171. promises: {
  172. setHistoryId: sinon.stub(),
  173. upgradeHistory: sinon.stub(),
  174. },
  175. }
  176. this.ProjectEntityUpdateHandler = {
  177. promises: {
  178. resyncProjectHistory: sinon.stub(),
  179. },
  180. }
  181. this.DocumentUpdaterHandler = {
  182. promises: {
  183. flushProjectToMongoAndDelete: sinon.stub(),
  184. },
  185. }
  186. this.HistoryManager = {
  187. promises: {
  188. resyncProject: sinon.stub(),
  189. flushProject: sinon.stub(),
  190. initializeProject: sinon.stub().resolves(historyId),
  191. },
  192. }
  193. this.settings = {
  194. redis: {
  195. project_history_migration: {
  196. key_schema: {
  197. projectHistoryOps({ projectId }) {
  198. return `ProjectHistory:Ops:{${projectId}}` // NOTE: the extra braces are intentional
  199. },
  200. },
  201. },
  202. },
  203. apis: {
  204. documentupdater: {
  205. url: 'http://document-updater',
  206. },
  207. trackchanges: {
  208. url: 'http://track-changes',
  209. },
  210. project_history: {
  211. url: 'http://project-history',
  212. },
  213. },
  214. path: {
  215. projectHistories: 'data/projectHistories',
  216. },
  217. }
  218. this.ProjectHistoryController = SandboxedModule.require(modulePath, {
  219. requires: {
  220. '../../../../app/src/Features/Project/ProjectGetter':
  221. this.ProjectGetter,
  222. '../../../../app/src/Features/FileStore/FileStoreHandler':
  223. this.FileStoreHandler,
  224. '../../../../app/src/Features/Project/ProjectHistoryHandler':
  225. this.ProjectHistoryHandler,
  226. '../../../../app/src/Features/Project/ProjectUpdateHandler':
  227. this.ProjectUpdateHandler,
  228. '../../../../app/src/Features/Project/ProjectEntityUpdateHandler':
  229. this.ProjectEntityUpdateHandler,
  230. '../../../../app/src/Features/History/HistoryManager':
  231. this.HistoryManager,
  232. '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler':
  233. this.DocumentUpdaterHandler,
  234. '../../../../app/src/models/Doc': {
  235. Doc: {
  236. find: this.findDocs,
  237. findOne: this.findOneDoc,
  238. },
  239. },
  240. '../../../../app/src/models/DeletedFile': {
  241. DeletedFile: {
  242. find: this.findDeletedFiles,
  243. },
  244. },
  245. '../../../../app/src/infrastructure/mongodb': {
  246. db: this.db,
  247. },
  248. '../../../../app/src/infrastructure/Mongoose': {
  249. Schema: {
  250. ObjectId: sinon.stub(),
  251. Types: {
  252. Mixed: sinon.stub(),
  253. },
  254. },
  255. },
  256. '../../../../app/src/infrastructure/RedisWrapper': {
  257. client: () => ({
  258. multi: () => this.multi,
  259. llen: sinon.stub().resolves(0),
  260. }),
  261. },
  262. unzipper: {
  263. Open: {
  264. file: () =>
  265. unzipper.Open.file(
  266. path.join(__dirname, 'data/track-changes-project.zip')
  267. ),
  268. },
  269. },
  270. '@overleaf/settings': this.settings,
  271. },
  272. })
  273. })
  274. afterEach(function () {
  275. nock.cleanAll()
  276. })
  277. it('migrates a project history', async function () {
  278. const readStream = fs.createReadStream(
  279. path.join(__dirname, 'data/track-changes-project.zip')
  280. )
  281. nock(this.settings.apis.trackchanges.url)
  282. .get(`/project/${projectId}/zip`)
  283. .reply(200, readStream)
  284. nock(this.settings.apis.project_history.url)
  285. .post(`/project`)
  286. .reply(200, { project: { id: historyId } })
  287. await this.ProjectHistoryController.migrateProjectHistory(
  288. projectId.toString(),
  289. 5
  290. )
  291. expect(this.multi.exec).to.have.been.calledOnce
  292. expect(this.ProjectHistoryHandler.promises.setHistoryId).to.have.been
  293. .calledOnce
  294. // expect(this.ProjectEntityUpdateHandler.promises.resyncProjectHistory).to
  295. // .have.been.calledOnce
  296. expect(this.HistoryManager.promises.flushProject).to.have.been.calledTwice
  297. expect(this.multi.rpush).to.have.callCount(12)
  298. const args = this.multi.rpush.args
  299. const snapshotPath = path.join(
  300. __dirname,
  301. 'data/migrate-project-history.snapshot.json'
  302. )
  303. // const snapshot = JSON.stringify(args, null, 2)
  304. // await fs.promises.writeFile(snapshotPath, snapshot)
  305. const json = await fs.promises.readFile(snapshotPath, 'utf-8')
  306. const expected = JSON.parse(json)
  307. expect(args).to.deep.equal(expected)
  308. })
  309. })