persist_changes.test.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. 'use strict'
  2. const { createHash } = require('node:crypto')
  3. const { expect } = require('chai')
  4. const cleanup = require('./support/cleanup')
  5. const fixtures = require('./support/fixtures')
  6. const storage = require('../../../../storage')
  7. const chunkStore = storage.chunkStore
  8. const persistChanges = storage.persistChanges
  9. const core = require('overleaf-editor-core')
  10. const AddFileOperation = core.AddFileOperation
  11. const EditFileOperation = core.EditFileOperation
  12. const TextOperation = core.TextOperation
  13. const Change = core.Change
  14. const Chunk = core.Chunk
  15. const File = core.File
  16. const History = core.History
  17. const Snapshot = core.Snapshot
  18. describe('persistChanges', function () {
  19. beforeEach(cleanup.everything)
  20. beforeEach(fixtures.create)
  21. let farFuture
  22. before(function () {
  23. // used to provide a limit which forces us to persist all of the changes.
  24. farFuture = new Date()
  25. farFuture.setTime(farFuture.getTime() + 7 * 24 * 3600 * 1000)
  26. })
  27. it('persists changes', async function () {
  28. const limitsToPersistImmediately = {
  29. minChangeTimestamp: farFuture,
  30. maxChangeTimestamp: farFuture,
  31. }
  32. const projectId = fixtures.docs.uninitializedProject.id
  33. const change = new Change(
  34. [new AddFileOperation('test.tex', File.fromString(''))],
  35. new Date(),
  36. []
  37. )
  38. const changes = [change]
  39. await chunkStore.initializeProject(projectId)
  40. const result = await persistChanges(
  41. projectId,
  42. changes,
  43. limitsToPersistImmediately,
  44. 0
  45. )
  46. const history = new History(new Snapshot(), changes)
  47. const currentChunk = new Chunk(history, 0)
  48. expect(result).to.deep.equal({
  49. numberOfChangesPersisted: 1,
  50. originalEndVersion: 0,
  51. currentChunk,
  52. resyncNeeded: false,
  53. })
  54. const chunk = await chunkStore.loadLatest(projectId)
  55. expect(chunk.getStartVersion()).to.equal(0)
  56. expect(chunk.getEndVersion()).to.equal(1)
  57. expect(chunk.getChanges().length).to.equal(1)
  58. })
  59. it('persists changes in three chunks', async function () {
  60. const limitsToPersistImmediately = {
  61. maxChunkChanges: 1,
  62. minChangeTimestamp: farFuture,
  63. maxChangeTimestamp: farFuture,
  64. }
  65. const projectId = fixtures.docs.uninitializedProject.id
  66. const firstChange = new Change(
  67. [new AddFileOperation('a.tex', File.fromString(''))],
  68. new Date(),
  69. []
  70. )
  71. const secondChange = new Change(
  72. [new AddFileOperation('b.tex', File.fromString(''))],
  73. new Date(),
  74. []
  75. )
  76. const thirdChange = new Change(
  77. [new AddFileOperation('c.tex', File.fromString(''))],
  78. new Date(),
  79. []
  80. )
  81. const changes = [firstChange, secondChange, thirdChange]
  82. await chunkStore.initializeProject(projectId)
  83. const result = await persistChanges(
  84. projectId,
  85. changes,
  86. limitsToPersistImmediately,
  87. 0
  88. )
  89. const snapshot = Snapshot.fromRaw({
  90. files: {
  91. 'a.tex': {
  92. content: '',
  93. },
  94. 'b.tex': {
  95. content: '',
  96. },
  97. },
  98. timestamp: thirdChange.getTimestamp().toISOString(),
  99. })
  100. const history = new History(snapshot, [thirdChange])
  101. const currentChunk = new Chunk(history, 2)
  102. expect(result).to.deep.equal({
  103. numberOfChangesPersisted: 3,
  104. originalEndVersion: 0,
  105. currentChunk,
  106. resyncNeeded: false,
  107. })
  108. const chunk = await chunkStore.loadLatest(projectId)
  109. expect(chunk.getStartVersion()).to.equal(2)
  110. expect(chunk.getEndVersion()).to.equal(3)
  111. expect(chunk.getChanges().length).to.equal(1)
  112. })
  113. it('persists the snapshot at the start of the chunk', async function () {
  114. const limitsToPersistImmediately = {
  115. maxChunkChanges: 2,
  116. minChangeTimestamp: farFuture,
  117. maxChangeTimestamp: farFuture,
  118. }
  119. const projectId = fixtures.docs.uninitializedProject.id
  120. const firstChange = new Change(
  121. [new AddFileOperation('a.tex', File.fromString(''))],
  122. new Date(),
  123. []
  124. )
  125. const secondChange = new Change(
  126. [new AddFileOperation('b.tex', File.fromString(''))],
  127. new Date(),
  128. []
  129. )
  130. const changes = [firstChange, secondChange]
  131. await chunkStore.initializeProject(projectId)
  132. const result = await persistChanges(
  133. projectId,
  134. changes,
  135. limitsToPersistImmediately,
  136. 0
  137. )
  138. const history = new History(new Snapshot(), changes)
  139. const currentChunk = new Chunk(history, 0)
  140. expect(result).to.deep.equal({
  141. numberOfChangesPersisted: 2,
  142. originalEndVersion: 0,
  143. currentChunk,
  144. resyncNeeded: false,
  145. })
  146. const chunk = await chunkStore.loadLatest(projectId)
  147. expect(chunk.getStartVersion()).to.equal(0)
  148. expect(chunk.getEndVersion()).to.equal(2)
  149. expect(chunk.getChanges().length).to.equal(2)
  150. })
  151. it("errors if the version doesn't match the latest chunk", async function () {
  152. const limitsToPersistImmediately = {
  153. minChangeTimestamp: farFuture,
  154. maxChangeTimestamp: farFuture,
  155. }
  156. const projectId = fixtures.docs.uninitializedProject.id
  157. const firstChange = new Change(
  158. [new AddFileOperation('a.tex', File.fromString(''))],
  159. new Date(),
  160. []
  161. )
  162. const secondChange = new Change(
  163. [new AddFileOperation('b.tex', File.fromString(''))],
  164. new Date(),
  165. []
  166. )
  167. const changes = [firstChange, secondChange]
  168. await chunkStore.initializeProject(projectId)
  169. await expect(
  170. persistChanges(projectId, changes, limitsToPersistImmediately, 1)
  171. ).to.be.rejectedWith(
  172. 'client sent updates with end_version 1 but latest chunk has end_version 0'
  173. )
  174. })
  175. describe('content hash validation', function () {
  176. it('acccepts a change with a valid hash', async function () {
  177. const limitsToPersistImmediately = {
  178. minChangeTimestamp: farFuture,
  179. maxChangeTimestamp: farFuture,
  180. }
  181. const projectId = fixtures.docs.uninitializedProject.id
  182. await chunkStore.initializeProject(projectId)
  183. const textOperation = new TextOperation()
  184. textOperation.insert('hello ')
  185. textOperation.retain(5)
  186. textOperation.contentHash = hashString('hello world')
  187. const change = new Change(
  188. [
  189. new AddFileOperation('a.tex', File.fromString('world')),
  190. new EditFileOperation('a.tex', textOperation),
  191. ],
  192. new Date(),
  193. []
  194. )
  195. const changes = [change]
  196. const result = await persistChanges(
  197. projectId,
  198. changes,
  199. limitsToPersistImmediately,
  200. 0
  201. )
  202. expect(result.numberOfChangesPersisted).to.equal(1)
  203. })
  204. it('turns on the resyncNeeded flag if content hash validation fails', async function () {
  205. const limitsToPersistImmediately = {
  206. minChangeTimestamp: farFuture,
  207. maxChangeTimestamp: farFuture,
  208. }
  209. const projectId = fixtures.docs.uninitializedProject.id
  210. await chunkStore.initializeProject(projectId)
  211. const textOperation = new TextOperation()
  212. textOperation.insert('hello ')
  213. textOperation.retain(5)
  214. textOperation.contentHash = hashString('bad hash')
  215. const change = new Change(
  216. [
  217. new AddFileOperation('a.tex', File.fromString('world')),
  218. new EditFileOperation('a.tex', textOperation),
  219. ],
  220. new Date(),
  221. []
  222. )
  223. const changes = [change]
  224. const result = await persistChanges(
  225. projectId,
  226. changes,
  227. limitsToPersistImmediately,
  228. 0
  229. )
  230. expect(result.resyncNeeded).to.be.true
  231. })
  232. })
  233. })
  234. function hashString(s) {
  235. const hash = createHash('sha-1')
  236. hash.update(s)
  237. return hash.digest('hex')
  238. }