HistoryManager.test.mjs 11 KB

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