persist_changes.test.js 7.1 KB

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