persist_changes.test.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  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 two 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 changes = [firstChange, secondChange]
  77. await chunkStore.initializeProject(projectId)
  78. const result = await persistChanges(
  79. projectId,
  80. changes,
  81. limitsToPersistImmediately,
  82. 0
  83. )
  84. const snapshot = Snapshot.fromRaw({
  85. files: {
  86. 'a.tex': {
  87. content: '',
  88. },
  89. },
  90. })
  91. const history = new History(snapshot, [secondChange])
  92. const currentChunk = new Chunk(history, 1)
  93. expect(result).to.deep.equal({
  94. numberOfChangesPersisted: 2,
  95. originalEndVersion: 0,
  96. currentChunk,
  97. resyncNeeded: false,
  98. })
  99. const chunk = await chunkStore.loadLatest(projectId)
  100. expect(chunk.getStartVersion()).to.equal(1)
  101. expect(chunk.getEndVersion()).to.equal(2)
  102. expect(chunk.getChanges().length).to.equal(1)
  103. })
  104. it('persists the snapshot at the start of the chunk', async function () {
  105. const limitsToPersistImmediately = {
  106. maxChunkChanges: 2,
  107. minChangeTimestamp: farFuture,
  108. maxChangeTimestamp: farFuture,
  109. }
  110. const projectId = fixtures.docs.uninitializedProject.id
  111. const firstChange = new Change(
  112. [new AddFileOperation('a.tex', File.fromString(''))],
  113. new Date(),
  114. []
  115. )
  116. const secondChange = new Change(
  117. [new AddFileOperation('b.tex', File.fromString(''))],
  118. new Date(),
  119. []
  120. )
  121. const changes = [firstChange, secondChange]
  122. await chunkStore.initializeProject(projectId)
  123. const result = await persistChanges(
  124. projectId,
  125. changes,
  126. limitsToPersistImmediately,
  127. 0
  128. )
  129. const history = new History(new Snapshot(), changes)
  130. const currentChunk = new Chunk(history, 0)
  131. expect(result).to.deep.equal({
  132. numberOfChangesPersisted: 2,
  133. originalEndVersion: 0,
  134. currentChunk,
  135. resyncNeeded: false,
  136. })
  137. const chunk = await chunkStore.loadLatest(projectId)
  138. expect(chunk.getStartVersion()).to.equal(0)
  139. expect(chunk.getEndVersion()).to.equal(2)
  140. expect(chunk.getChanges().length).to.equal(2)
  141. })
  142. it("errors if the version doesn't match the latest chunk", async function () {
  143. const limitsToPersistImmediately = {
  144. minChangeTimestamp: farFuture,
  145. maxChangeTimestamp: farFuture,
  146. }
  147. const projectId = fixtures.docs.uninitializedProject.id
  148. const firstChange = new Change(
  149. [new AddFileOperation('a.tex', File.fromString(''))],
  150. new Date(),
  151. []
  152. )
  153. const secondChange = new Change(
  154. [new AddFileOperation('b.tex', File.fromString(''))],
  155. new Date(),
  156. []
  157. )
  158. const changes = [firstChange, secondChange]
  159. await chunkStore.initializeProject(projectId)
  160. await expect(
  161. persistChanges(projectId, changes, limitsToPersistImmediately, 1)
  162. ).to.be.rejectedWith(
  163. 'client sent updates with end_version 1 but latest chunk has end_version 0'
  164. )
  165. })
  166. describe('content hash validation', function () {
  167. it('acccepts a change with a valid hash', async function () {
  168. const limitsToPersistImmediately = {
  169. minChangeTimestamp: farFuture,
  170. maxChangeTimestamp: farFuture,
  171. }
  172. const projectId = fixtures.docs.uninitializedProject.id
  173. await chunkStore.initializeProject(projectId)
  174. const textOperation = new TextOperation()
  175. textOperation.insert('hello ')
  176. textOperation.retain(5)
  177. textOperation.contentHash = hashString('hello world')
  178. const change = new Change(
  179. [
  180. new AddFileOperation('a.tex', File.fromString('world')),
  181. new EditFileOperation('a.tex', textOperation),
  182. ],
  183. new Date(),
  184. []
  185. )
  186. const changes = [change]
  187. const result = await persistChanges(
  188. projectId,
  189. changes,
  190. limitsToPersistImmediately,
  191. 0
  192. )
  193. expect(result.numberOfChangesPersisted).to.equal(1)
  194. })
  195. it('turns on the resyncNeeded flag if content hash validation fails', async function () {
  196. const limitsToPersistImmediately = {
  197. minChangeTimestamp: farFuture,
  198. maxChangeTimestamp: farFuture,
  199. }
  200. const projectId = fixtures.docs.uninitializedProject.id
  201. await chunkStore.initializeProject(projectId)
  202. const textOperation = new TextOperation()
  203. textOperation.insert('hello ')
  204. textOperation.retain(5)
  205. textOperation.contentHash = hashString('bad hash')
  206. const change = new Change(
  207. [
  208. new AddFileOperation('a.tex', File.fromString('world')),
  209. new EditFileOperation('a.tex', textOperation),
  210. ],
  211. new Date(),
  212. []
  213. )
  214. const changes = [change]
  215. const result = await persistChanges(
  216. projectId,
  217. changes,
  218. limitsToPersistImmediately,
  219. 0
  220. )
  221. expect(result.resyncNeeded).to.be.true
  222. })
  223. })
  224. })
  225. function hashString(s) {
  226. const hash = createHash('sha-1')
  227. hash.update(s)
  228. return hash.digest('hex')
  229. }