DiffTests.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. import { expect } from 'chai'
  2. import crypto from 'node:crypto'
  3. import mongodb from 'mongodb-legacy'
  4. import nock from 'nock'
  5. import {
  6. fetchJsonWithResponse,
  7. RequestFailedError,
  8. } from '@overleaf/fetch-utils'
  9. import * as ProjectHistoryClient from './helpers/ProjectHistoryClient.js'
  10. import * as ProjectHistoryApp from './helpers/ProjectHistoryApp.js'
  11. const { ObjectId } = mongodb
  12. const MockHistoryStore = () => nock('http://127.0.0.1:3100')
  13. const MockWeb = () => nock('http://127.0.0.1:3000')
  14. function createMockBlob(historyId, content) {
  15. const sha = crypto.createHash('sha1').update(content).digest('hex')
  16. MockHistoryStore()
  17. .get(`/api/projects/${historyId}/blobs/${sha}`)
  18. .reply(200, content)
  19. .persist()
  20. return sha
  21. }
  22. describe('Diffs', function () {
  23. beforeEach(async function () {
  24. await ProjectHistoryApp.ensureRunning()
  25. this.historyId = new ObjectId().toString()
  26. this.projectId = new ObjectId().toString()
  27. MockHistoryStore().post('/api/projects').reply(200, {
  28. projectId: this.historyId,
  29. })
  30. MockWeb()
  31. .get(`/project/${this.projectId}/details`)
  32. .reply(200, {
  33. name: 'Test Project',
  34. overleaf: { history: { id: this.historyId } },
  35. })
  36. await ProjectHistoryClient.initializeProject(this.historyId)
  37. })
  38. afterEach(function () {
  39. nock.cleanAll()
  40. })
  41. it('should return a diff of the updates to a doc from a single chunk', async function () {
  42. this.blob = 'one two three five'
  43. this.sha = createMockBlob(this.historyId, this.blob)
  44. this.v2AuthorId = '123456789'
  45. MockHistoryStore()
  46. .get(`/api/projects/${this.historyId}/versions/6/history`)
  47. .reply(200, {
  48. chunk: {
  49. history: {
  50. snapshot: {
  51. files: {
  52. 'foo.tex': {
  53. hash: this.sha,
  54. stringLength: this.blob.length,
  55. },
  56. },
  57. },
  58. changes: [
  59. {
  60. operations: [
  61. {
  62. pathname: 'foo.tex',
  63. textOperation: [13, ' four', 5],
  64. },
  65. ],
  66. timestamp: '2017-12-04T10:29:17.786Z',
  67. authors: [31],
  68. },
  69. {
  70. operations: [
  71. {
  72. pathname: 'foo.tex',
  73. textOperation: [4, -4, 15],
  74. },
  75. ],
  76. timestamp: '2017-12-04T10:29:22.905Z',
  77. authors: [31],
  78. },
  79. {
  80. operations: [
  81. {
  82. pathname: 'foo.tex',
  83. textOperation: [19, ' six'],
  84. },
  85. ],
  86. timestamp: '2017-12-04T10:29:26.120Z',
  87. v2Authors: [this.v2AuthorId],
  88. },
  89. ],
  90. },
  91. startVersion: 3,
  92. },
  93. authors: [31],
  94. })
  95. const diff = await ProjectHistoryClient.getDiff(
  96. this.projectId,
  97. 'foo.tex',
  98. 3,
  99. 6
  100. )
  101. expect(diff).to.deep.equal({
  102. diff: [
  103. {
  104. u: 'one ',
  105. },
  106. {
  107. d: 'two ',
  108. meta: {
  109. users: [31],
  110. start_ts: 1512383362905,
  111. end_ts: 1512383362905,
  112. },
  113. },
  114. {
  115. u: 'three',
  116. },
  117. {
  118. i: ' four',
  119. meta: {
  120. users: [31],
  121. start_ts: 1512383357786,
  122. end_ts: 1512383357786,
  123. },
  124. },
  125. {
  126. u: ' five',
  127. },
  128. {
  129. i: ' six',
  130. meta: {
  131. users: [this.v2AuthorId],
  132. start_ts: 1512383366120,
  133. end_ts: 1512383366120,
  134. },
  135. },
  136. ],
  137. })
  138. })
  139. it('should return a diff of the updates to a doc across multiple chunks', async function () {
  140. MockHistoryStore()
  141. .get(`/api/projects/${this.historyId}/versions/5/history`)
  142. .reply(200, {
  143. chunk: {
  144. history: {
  145. snapshot: {
  146. files: {
  147. 'foo.tex': {
  148. hash: createMockBlob(this.historyId, 'one two three five'),
  149. stringLength: 'one three four five'.length,
  150. },
  151. },
  152. },
  153. changes: [
  154. {
  155. operations: [
  156. {
  157. pathname: 'foo.tex',
  158. textOperation: [13, ' four', 5],
  159. },
  160. ],
  161. timestamp: '2017-12-04T10:29:17.786Z',
  162. authors: [31],
  163. },
  164. {
  165. operations: [
  166. {
  167. pathname: 'foo.tex',
  168. textOperation: [4, -4, 15],
  169. },
  170. ],
  171. timestamp: '2017-12-04T10:29:22.905Z',
  172. authors: [31],
  173. },
  174. ],
  175. },
  176. startVersion: 3,
  177. },
  178. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  179. })
  180. MockHistoryStore()
  181. .get(`/api/projects/${this.historyId}/versions/6/history`)
  182. .reply(200, {
  183. chunk: {
  184. history: {
  185. snapshot: {
  186. files: {
  187. 'foo.tex': {
  188. hash: createMockBlob(this.historyId, 'one three four five'),
  189. stringLength: 'one three four five'.length,
  190. },
  191. },
  192. },
  193. changes: [
  194. {
  195. operations: [
  196. {
  197. pathname: 'foo.tex',
  198. textOperation: [19, ' six'],
  199. },
  200. ],
  201. timestamp: '2017-12-04T10:29:26.120Z',
  202. authors: [31],
  203. },
  204. {
  205. operations: [
  206. {
  207. pathname: 'foo.tex',
  208. textOperation: [23, ' seven'],
  209. },
  210. ],
  211. timestamp: '2017-12-04T10:29:26.120Z',
  212. authors: [31],
  213. },
  214. ],
  215. },
  216. startVersion: 5,
  217. },
  218. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  219. })
  220. const diff = await ProjectHistoryClient.getDiff(
  221. this.projectId,
  222. 'foo.tex',
  223. 4,
  224. 6
  225. )
  226. expect(diff).to.deep.equal({
  227. diff: [
  228. {
  229. u: 'one ',
  230. },
  231. {
  232. d: 'two ',
  233. meta: {
  234. users: [31],
  235. start_ts: 1512383362905,
  236. end_ts: 1512383362905,
  237. },
  238. },
  239. {
  240. u: 'three four five',
  241. },
  242. {
  243. i: ' six',
  244. meta: {
  245. users: [31],
  246. start_ts: 1512383366120,
  247. end_ts: 1512383366120,
  248. },
  249. },
  250. ],
  251. })
  252. })
  253. it('should return a 404 when there are no changes for the file in the range', async function () {
  254. this.blob = 'one two three five'
  255. this.sha = createMockBlob(this.historyId, this.blob)
  256. MockHistoryStore()
  257. .get(`/api/projects/${this.historyId}/versions/6/history`)
  258. .reply(200, {
  259. chunk: {
  260. history: {
  261. snapshot: {
  262. files: {
  263. 'foo.tex': {
  264. hash: this.sha,
  265. stringLength: this.blob.length,
  266. },
  267. },
  268. },
  269. changes: [
  270. {
  271. operations: [
  272. {
  273. pathname: 'foo.tex',
  274. textOperation: [13, ' four', 5],
  275. },
  276. ],
  277. timestamp: '2017-12-04T10:29:17.786Z',
  278. authors: [31],
  279. },
  280. ],
  281. },
  282. startVersion: 3,
  283. },
  284. authors: [31],
  285. })
  286. try {
  287. await fetchJsonWithResponse(
  288. `http://127.0.0.1:3054/project/${this.projectId}/diff?pathname=not_here.tex&from=3&to=6`
  289. )
  290. expect.fail('Expected a 404 error')
  291. } catch (error) {
  292. expect(error).to.be.instanceOf(RequestFailedError)
  293. expect(error.response.status).to.equal(404)
  294. }
  295. })
  296. it('should return a binary flag with a diff of a binary file', async function () {
  297. this.blob = 'one two three five'
  298. this.sha = createMockBlob(this.historyId, this.blob)
  299. this.binaryBlob = Buffer.from([1, 2, 3, 4])
  300. this.binarySha = createMockBlob(this.historyId, this.binaryBlob)
  301. MockHistoryStore()
  302. .get(`/api/projects/${this.historyId}/versions/6/history`)
  303. .reply(200, {
  304. chunk: {
  305. history: {
  306. snapshot: {
  307. files: {
  308. 'binary.tex': {
  309. hash: this.binarySha,
  310. byteLength: this.binaryBlob.length, // Indicates binary
  311. },
  312. 'foo.tex': {
  313. hash: this.sha,
  314. stringLength: this.blob.length, // Indicates binary
  315. },
  316. },
  317. },
  318. changes: [
  319. {
  320. operations: [
  321. {
  322. pathname: 'foo.tex',
  323. textOperation: [13, ' four', 5],
  324. },
  325. ],
  326. timestamp: '2017-12-04T10:29:17.786Z',
  327. authors: [31],
  328. },
  329. {
  330. operations: [
  331. {
  332. pathname: 'foo.tex',
  333. textOperation: [4, -4, 15],
  334. },
  335. ],
  336. timestamp: '2017-12-04T10:29:22.905Z',
  337. authors: [31],
  338. },
  339. {
  340. operations: [
  341. {
  342. pathname: 'foo.tex',
  343. textOperation: [19, ' six'],
  344. },
  345. ],
  346. timestamp: '2017-12-04T10:29:26.120Z',
  347. authors: [31],
  348. },
  349. ],
  350. },
  351. startVersion: 3,
  352. },
  353. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  354. })
  355. const diff = await ProjectHistoryClient.getDiff(
  356. this.projectId,
  357. 'binary.tex',
  358. 3,
  359. 6
  360. )
  361. expect(diff).to.deep.equal({
  362. diff: {
  363. binary: true,
  364. },
  365. })
  366. })
  367. })