HistoryManager.sequential.test.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. import { beforeAll, beforeEach, describe, it, vi, expect } from 'vitest'
  2. import sinon from 'sinon'
  3. import mongodb from 'mongodb-legacy'
  4. import {
  5. cleanupTestDatabase,
  6. db,
  7. waitForDb,
  8. } from '../../../../app/src/infrastructure/mongodb.mjs'
  9. const { ObjectId } = mongodb
  10. const MODULE_PATH = '../../../../app/src/Features/History/HistoryManager'
  11. const GLOBAL_BLOBS = [
  12. 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391',
  13. '02426c2b3a484003ca42ed52b374b7907b757d12',
  14. ]
  15. describe('HistoryManager', function () {
  16. beforeAll(async function () {
  17. await waitForDb()
  18. })
  19. beforeAll(cleanupTestDatabase)
  20. beforeAll(async function () {
  21. await db.projectHistoryGlobalBlobs.insertMany(
  22. GLOBAL_BLOBS.map(sha => ({
  23. _id: sha,
  24. byteLength: 0,
  25. stringLength: 0,
  26. }))
  27. )
  28. })
  29. beforeEach(async function (ctx) {
  30. ctx.user_id = 'user-id-123'
  31. ctx.historyId = new ObjectId().toString()
  32. ctx.AuthenticationController = {
  33. getLoggedInUserId: sinon.stub().returns(ctx.user_id),
  34. }
  35. ctx.FetchUtils = {
  36. fetchJson: sinon.stub(),
  37. fetchNothing: sinon.stub().resolves(),
  38. }
  39. ctx.projectHistoryUrl = 'http://project_history.example.com'
  40. ctx.v1HistoryUrl = 'http://v1_history.example.com'
  41. ctx.v1HistoryUser = 'system'
  42. ctx.v1HistoryPassword = 'verysecret'
  43. ctx.settings = {
  44. apis: {
  45. filestore: {
  46. url: 'http://filestore.example.com',
  47. },
  48. project_history: {
  49. url: ctx.projectHistoryUrl,
  50. },
  51. v1_history: {
  52. url: ctx.v1HistoryUrl,
  53. user: ctx.v1HistoryUser,
  54. pass: ctx.v1HistoryPassword,
  55. buckets: {
  56. globalBlobs: 'globalBlobs',
  57. projectBlobs: 'projectBlobs',
  58. },
  59. },
  60. },
  61. }
  62. ctx.UserGetter = {
  63. promises: {
  64. getUsersByV1Ids: sinon.stub(),
  65. getUsers: sinon.stub(),
  66. },
  67. }
  68. ctx.project = {
  69. overleaf: {
  70. history: {
  71. id: ctx.historyId,
  72. },
  73. },
  74. }
  75. ctx.ProjectGetter = {
  76. promises: {
  77. getProject: sinon.stub().resolves(ctx.project),
  78. },
  79. }
  80. ctx.HistoryBackupDeletionHandler = {
  81. deleteProject: sinon.stub().resolves(),
  82. }
  83. vi.doMock('../../../../app/src/infrastructure/mongodb', () => ({
  84. ObjectId,
  85. db,
  86. waitForDb,
  87. }))
  88. vi.doMock('@overleaf/fetch-utils', () => ctx.FetchUtils)
  89. vi.doMock('@overleaf/settings', () => ({
  90. default: ctx.settings,
  91. }))
  92. vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
  93. default: ctx.UserGetter,
  94. }))
  95. vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
  96. default: ctx.ProjectGetter,
  97. }))
  98. vi.doMock(
  99. '../../../../app/src/Features/History/HistoryBackupDeletionHandler',
  100. () => ({
  101. default: ctx.HistoryBackupDeletionHandler,
  102. })
  103. )
  104. ctx.HistoryManager = (await import(MODULE_PATH)).default
  105. })
  106. describe('getFilestoreBlobURL', function () {
  107. beforeEach(async function (ctx) {
  108. await ctx.HistoryManager.loadGlobalBlobsPromise
  109. })
  110. it('should return a global blob location', function (ctx) {
  111. for (const sha of GLOBAL_BLOBS) {
  112. expect(ctx.HistoryManager.getFilestoreBlobURL('42', sha)).to.equal(
  113. `${ctx.settings.apis.filestore.url}/history/global/hash/${sha}`
  114. )
  115. }
  116. })
  117. it('should return a project blob location for a v1 project', function (ctx) {
  118. const historyId = 42
  119. const sha = '6ddfa0578a67fe5ad6623a8665ec9aafce1eb5ca'
  120. expect(ctx.HistoryManager.getFilestoreBlobURL(historyId, sha)).to.equal(
  121. `${ctx.settings.apis.filestore.url}/history/project/${historyId}/hash/${sha}`
  122. )
  123. })
  124. it('should return a project blob location for a mongo project', function (ctx) {
  125. const historyId = '424242424242424242424242'
  126. const sha = '6ddfa0578a67fe5ad6623a8665ec9aafce1eb5ca'
  127. expect(ctx.HistoryManager.getFilestoreBlobURL(historyId, sha)).to.equal(
  128. `${ctx.settings.apis.filestore.url}/history/project/${historyId}/hash/${sha}`
  129. )
  130. })
  131. })
  132. describe('initializeProject', function () {
  133. beforeEach(function (ctx) {
  134. ctx.settings.apis.project_history.initializeHistoryForNewProjects = true
  135. })
  136. describe('project history returns a successful response', function () {
  137. beforeEach(async function (ctx) {
  138. ctx.FetchUtils.fetchJson.resolves({ project: { id: ctx.historyId } })
  139. ctx.result = await ctx.HistoryManager.promises.initializeProject(
  140. ctx.historyId
  141. )
  142. })
  143. it('should call the project history api', function (ctx) {
  144. ctx.FetchUtils.fetchJson.should.have.been.calledWithMatch(
  145. `${ctx.settings.apis.project_history.url}/project`,
  146. { method: 'POST' }
  147. )
  148. })
  149. it('should return the overleaf id', function (ctx) {
  150. expect(ctx.result).to.equal(ctx.historyId)
  151. })
  152. })
  153. describe('project history returns a response without the project id', function () {
  154. it('should throw an error', async function (ctx) {
  155. ctx.FetchUtils.fetchJson.resolves({ project: {} })
  156. await expect(
  157. ctx.HistoryManager.promises.initializeProject(ctx.historyId)
  158. ).to.be.rejected
  159. })
  160. })
  161. describe('project history errors', function () {
  162. it('should propagate the error', async function (ctx) {
  163. ctx.FetchUtils.fetchJson.rejects(new Error('problem connecting'))
  164. await expect(
  165. ctx.HistoryManager.promises.initializeProject(ctx.historyId)
  166. ).to.be.rejected
  167. })
  168. })
  169. })
  170. describe('injectUserDetails', function () {
  171. beforeEach(function (ctx) {
  172. ctx.user1 = {
  173. _id: (ctx.user_id1 = '123456'),
  174. first_name: 'Jane',
  175. last_name: 'Doe',
  176. email: 'jane@example.com',
  177. overleaf: { id: 5011 },
  178. }
  179. ctx.user1_view = {
  180. id: ctx.user_id1,
  181. first_name: 'Jane',
  182. last_name: 'Doe',
  183. email: 'jane@example.com',
  184. }
  185. ctx.user2 = {
  186. _id: (ctx.user_id2 = 'abcdef'),
  187. first_name: 'John',
  188. last_name: 'Doe',
  189. email: 'john@example.com',
  190. }
  191. ctx.user2_view = {
  192. id: ctx.user_id2,
  193. first_name: 'John',
  194. last_name: 'Doe',
  195. email: 'john@example.com',
  196. }
  197. ctx.UserGetter.promises.getUsersByV1Ids.resolves([ctx.user1])
  198. ctx.UserGetter.promises.getUsers.resolves([ctx.user1, ctx.user2])
  199. })
  200. describe('with a diff', function () {
  201. it('should turn user_ids into user objects', async function (ctx) {
  202. const diff = await ctx.HistoryManager.promises.injectUserDetails({
  203. diff: [
  204. {
  205. i: 'foo',
  206. meta: {
  207. users: [ctx.user_id1],
  208. },
  209. },
  210. {
  211. i: 'bar',
  212. meta: {
  213. users: [ctx.user_id2],
  214. },
  215. },
  216. ],
  217. })
  218. expect(diff.diff[0].meta.users).to.deep.equal([ctx.user1_view])
  219. expect(diff.diff[1].meta.users).to.deep.equal([ctx.user2_view])
  220. })
  221. it('should handle v1 user ids', async function (ctx) {
  222. const diff = await ctx.HistoryManager.promises.injectUserDetails({
  223. diff: [
  224. {
  225. i: 'foo',
  226. meta: {
  227. users: [5011],
  228. },
  229. },
  230. {
  231. i: 'bar',
  232. meta: {
  233. users: [ctx.user_id2],
  234. },
  235. },
  236. ],
  237. })
  238. expect(diff.diff[0].meta.users).to.deep.equal([ctx.user1_view])
  239. expect(diff.diff[1].meta.users).to.deep.equal([ctx.user2_view])
  240. })
  241. it('should leave user objects', async function (ctx) {
  242. const diff = await ctx.HistoryManager.promises.injectUserDetails({
  243. diff: [
  244. {
  245. i: 'foo',
  246. meta: {
  247. users: [ctx.user1_view],
  248. },
  249. },
  250. {
  251. i: 'bar',
  252. meta: {
  253. users: [ctx.user_id2],
  254. },
  255. },
  256. ],
  257. })
  258. expect(diff.diff[0].meta.users).to.deep.equal([ctx.user1_view])
  259. expect(diff.diff[1].meta.users).to.deep.equal([ctx.user2_view])
  260. })
  261. it('should handle a binary diff marker', async function (ctx) {
  262. const diff = await ctx.HistoryManager.promises.injectUserDetails({
  263. diff: { binary: true },
  264. })
  265. expect(diff.diff.binary).to.be.true
  266. })
  267. })
  268. describe('with a list of updates', function () {
  269. it('should turn user_ids into user objects', async function (ctx) {
  270. const updates = await ctx.HistoryManager.promises.injectUserDetails({
  271. updates: [
  272. {
  273. fromV: 5,
  274. toV: 8,
  275. meta: {
  276. users: [ctx.user_id1],
  277. },
  278. },
  279. {
  280. fromV: 4,
  281. toV: 5,
  282. meta: {
  283. users: [ctx.user_id2],
  284. },
  285. },
  286. ],
  287. })
  288. expect(updates.updates[0].meta.users).to.deep.equal([ctx.user1_view])
  289. expect(updates.updates[1].meta.users).to.deep.equal([ctx.user2_view])
  290. })
  291. it('should leave user objects', async function (ctx) {
  292. const updates = await ctx.HistoryManager.promises.injectUserDetails({
  293. updates: [
  294. {
  295. fromV: 5,
  296. toV: 8,
  297. meta: {
  298. users: [ctx.user1_view],
  299. },
  300. },
  301. {
  302. fromV: 4,
  303. toV: 5,
  304. meta: {
  305. users: [ctx.user_id2],
  306. },
  307. },
  308. ],
  309. })
  310. expect(updates.updates[0].meta.users).to.deep.equal([ctx.user1_view])
  311. expect(updates.updates[1].meta.users).to.deep.equal([ctx.user2_view])
  312. })
  313. })
  314. })
  315. describe('deleteProject', function () {
  316. const projectId = new ObjectId()
  317. const historyId = new ObjectId()
  318. beforeEach(async function (ctx) {
  319. await ctx.HistoryManager.promises.deleteProject(projectId, historyId)
  320. })
  321. it('should call the project-history service', async function (ctx) {
  322. expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWith(
  323. `${ctx.projectHistoryUrl}/project/${projectId}`,
  324. { method: 'DELETE' }
  325. )
  326. })
  327. it('should call the v1-history service', async function (ctx) {
  328. expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWith(
  329. `${ctx.v1HistoryUrl}/projects/${historyId}`,
  330. {
  331. method: 'DELETE',
  332. basicAuth: {
  333. user: ctx.v1HistoryUser,
  334. password: ctx.v1HistoryPassword,
  335. },
  336. }
  337. )
  338. })
  339. it('should call the history-backup-deletion service', async function (ctx) {
  340. expect(
  341. ctx.HistoryBackupDeletionHandler.deleteProject
  342. ).to.have.been.calledWith(projectId)
  343. })
  344. })
  345. })