end_to_end.test.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. 'use strict'
  2. const BPromise = require('bluebird')
  3. const { expect } = require('chai')
  4. const HTTPStatus = require('http-status')
  5. const fetch = require('node-fetch')
  6. const fs = BPromise.promisifyAll(require('node:fs'))
  7. const cleanup = require('../storage/support/cleanup')
  8. const fixtures = require('../storage/support/fixtures')
  9. const testFiles = require('../storage/support/test_files')
  10. const testProjects = require('./support/test_projects')
  11. const testServer = require('./support/test_server')
  12. const core = require('overleaf-editor-core')
  13. const Change = core.Change
  14. const ChunkResponse = core.ChunkResponse
  15. const File = core.File
  16. const Operation = core.Operation
  17. const Snapshot = core.Snapshot
  18. const TextOperation = core.TextOperation
  19. const blobHash = require('../../../../storage').blobHash
  20. describe('overleaf ot', function () {
  21. beforeEach(cleanup.everything)
  22. beforeEach(fixtures.create)
  23. this.timeout(10000) // it takes a while on Docker for Mac
  24. it('can use API', function () {
  25. let client, downloadZipClient
  26. const basicAuthClient = testServer.basicAuthClient
  27. return (
  28. testProjects
  29. .createEmptyProject()
  30. .then(projectId => {
  31. return testServer
  32. .createClientForProject(projectId)
  33. .then(clientForProject => {
  34. client = clientForProject
  35. return testServer.createClientForDownloadZip(projectId)
  36. })
  37. .then(clientForProject => {
  38. downloadZipClient = clientForProject
  39. return projectId
  40. })
  41. })
  42. // the project is currently empty
  43. .then(projectId => {
  44. return client.apis.Project.getLatestContent({
  45. project_id: projectId,
  46. }).then(response => {
  47. const snapshot = Snapshot.fromRaw(response.obj)
  48. expect(snapshot.countFiles()).to.equal(0)
  49. return projectId
  50. })
  51. })
  52. // upload a blob and add two files using it
  53. .then(projectId => {
  54. return fetch(
  55. testServer.url(
  56. `/api/projects/${projectId}/blobs/${testFiles.GRAPH_PNG_HASH}`,
  57. { qs: { pathname: 'graph_1.png' } }
  58. ),
  59. {
  60. method: 'PUT',
  61. body: fs.createReadStream(testFiles.path('graph.png')),
  62. headers: {
  63. Authorization: testServer.basicAuthHeader,
  64. },
  65. }
  66. )
  67. .then(response => {
  68. expect(response.ok).to.be.true
  69. })
  70. .then(() => {
  71. const testFile = File.fromHash(testFiles.GRAPH_PNG_HASH)
  72. const change = new Change(
  73. [
  74. Operation.addFile('graph_1.png', testFile),
  75. Operation.addFile('graph_2.png', testFile),
  76. ],
  77. new Date()
  78. )
  79. return basicAuthClient.apis.ProjectImport.importChanges1({
  80. project_id: projectId,
  81. end_version: 0,
  82. return_snapshot: 'hashed',
  83. changes: [change.toRaw()],
  84. })
  85. })
  86. .then(() => projectId)
  87. })
  88. // get the new project state
  89. .then(projectId => {
  90. return client.apis.Project.getLatestContent({
  91. project_id: projectId,
  92. }).then(response => {
  93. const snapshot = Snapshot.fromRaw(response.obj)
  94. expect(snapshot.countFiles()).to.equal(2)
  95. const file0 = snapshot.getFile('graph_1.png')
  96. expect(file0.getHash()).to.equal(testFiles.GRAPH_PNG_HASH)
  97. const file1 = snapshot.getFile('graph_2.png')
  98. expect(file1.getHash()).to.equal(testFiles.GRAPH_PNG_HASH)
  99. return projectId
  100. })
  101. })
  102. // get the history
  103. .then(projectId => {
  104. return client.apis.Project.getLatestHistory({
  105. project_id: projectId,
  106. }).then(response => {
  107. const chunk = ChunkResponse.fromRaw(response.obj).getChunk()
  108. const changes = chunk.getChanges()
  109. expect(changes.length).to.equal(1)
  110. const change0Timestamp = changes[0].getTimestamp().getTime()
  111. expect(change0Timestamp).to.be.closeTo(Date.now(), 1e4)
  112. return projectId
  113. })
  114. })
  115. // upload an empty file
  116. .then(projectId => {
  117. return fetch(
  118. testServer.url(
  119. `/api/projects/${projectId}/blobs/${File.EMPTY_FILE_HASH}`,
  120. { qs: { pathname: 'main.tex' } }
  121. ),
  122. {
  123. method: 'PUT',
  124. body: fs.createReadStream(testFiles.path('empty.tex')),
  125. headers: {
  126. Authorization: testServer.basicAuthHeader,
  127. },
  128. }
  129. )
  130. .then(response => {
  131. expect(response.ok).to.be.true
  132. })
  133. .then(() => {
  134. const testFile = File.fromHash(File.EMPTY_FILE_HASH)
  135. const change = new Change(
  136. [Operation.addFile('main.tex', testFile)],
  137. new Date()
  138. )
  139. return basicAuthClient.apis.ProjectImport.importChanges1({
  140. project_id: projectId,
  141. end_version: 1,
  142. return_snapshot: 'hashed',
  143. changes: [change.toRaw()],
  144. })
  145. })
  146. .then(() => projectId)
  147. })
  148. .then(projectId => {
  149. // Fetch empty file blob
  150. return client.apis.Project.getProjectBlob({
  151. project_id: projectId,
  152. hash: File.EMPTY_FILE_HASH,
  153. })
  154. .then(response => {
  155. expect(response.headers['content-type']).to.equal(
  156. 'application/octet-stream'
  157. )
  158. return response.data.arrayBuffer()
  159. })
  160. .then(buffer => {
  161. expect(buffer).to.deep.equal(new ArrayBuffer(0))
  162. return projectId
  163. })
  164. })
  165. // get the history
  166. .then(projectId => {
  167. return client.apis.Project.getLatestHistory({
  168. project_id: projectId,
  169. }).then(response => {
  170. const chunk = ChunkResponse.fromRaw(response.obj).getChunk()
  171. const changes = chunk.getChanges()
  172. expect(changes.length).to.equal(2)
  173. return projectId
  174. })
  175. })
  176. // get the new project state
  177. .then(projectId => {
  178. return client.apis.Project.getLatestContent({
  179. project_id: projectId,
  180. }).then(response => {
  181. const snapshot = Snapshot.fromRaw(response.obj)
  182. expect(snapshot.countFiles()).to.equal(3)
  183. expect(snapshot.getFile('graph_1.png').getHash()).to.equal(
  184. testFiles.GRAPH_PNG_HASH
  185. )
  186. expect(snapshot.getFile('graph_2.png').getHash()).to.equal(
  187. testFiles.GRAPH_PNG_HASH
  188. )
  189. expect(snapshot.getFile('main.tex').getContent()).to.equal('')
  190. return projectId
  191. })
  192. })
  193. // edit the main file
  194. .then(projectId => {
  195. const change = new Change(
  196. [
  197. Operation.editFile(
  198. 'main.tex',
  199. TextOperation.fromJSON({ textOperation: ['hello'] })
  200. ),
  201. ],
  202. new Date()
  203. )
  204. return basicAuthClient.apis.ProjectImport.importChanges1({
  205. project_id: projectId,
  206. changes: [change.toRaw()],
  207. end_version: 2,
  208. return_snapshot: 'hashed',
  209. }).then(response => {
  210. expect(response.status).to.equal(HTTPStatus.CREATED)
  211. const snapshot = Snapshot.fromRaw(response.obj)
  212. expect(snapshot.countFiles()).to.equal(3)
  213. expect(snapshot.getFile('graph_1.png').getHash()).to.equal(
  214. testFiles.GRAPH_PNG_HASH
  215. )
  216. expect(snapshot.getFile('graph_2.png').getHash()).to.equal(
  217. testFiles.GRAPH_PNG_HASH
  218. )
  219. expect(snapshot.getFile('main.tex').getHash()).to.equal(
  220. blobHash.fromString('hello')
  221. )
  222. return projectId
  223. })
  224. })
  225. // get the new project state
  226. .then(projectId => {
  227. return client.apis.Project.getLatestContent({
  228. project_id: projectId,
  229. }).then(response => {
  230. const snapshot = Snapshot.fromRaw(response.obj)
  231. expect(snapshot.countFiles()).to.equal(3)
  232. expect(snapshot.getFile('graph_1.png').getHash()).to.equal(
  233. testFiles.GRAPH_PNG_HASH
  234. )
  235. expect(snapshot.getFile('graph_2.png').getHash()).to.equal(
  236. testFiles.GRAPH_PNG_HASH
  237. )
  238. const mainFile = snapshot.getFile('main.tex')
  239. expect(mainFile.getHash()).to.be.null
  240. expect(mainFile.getContent()).to.equal('hello')
  241. return projectId
  242. })
  243. })
  244. // edit the main file again
  245. .then(projectId => {
  246. const change = new Change(
  247. [
  248. Operation.editFile(
  249. 'main.tex',
  250. TextOperation.fromJSON({ textOperation: [1, -4, 'i world'] })
  251. ),
  252. ],
  253. new Date()
  254. )
  255. return basicAuthClient.apis.ProjectImport.importChanges1({
  256. project_id: projectId,
  257. changes: [change.toRaw()],
  258. end_version: 3,
  259. return_snapshot: 'hashed',
  260. }).then(response => {
  261. expect(response.status).to.equal(HTTPStatus.CREATED)
  262. const snapshot = Snapshot.fromRaw(response.obj)
  263. expect(snapshot.countFiles()).to.equal(3)
  264. expect(snapshot.getFile('main.tex').getHash()).to.equal(
  265. blobHash.fromString('hi world')
  266. )
  267. return projectId
  268. })
  269. })
  270. // get the new project state
  271. .then(projectId => {
  272. return client.apis.Project.getLatestContent({
  273. project_id: projectId,
  274. }).then(response => {
  275. const snapshot = Snapshot.fromRaw(response.obj)
  276. expect(snapshot.countFiles()).to.equal(3)
  277. expect(snapshot.getFile('graph_1.png')).to.exist
  278. expect(snapshot.getFile('graph_2.png')).to.exist
  279. const mainFile = snapshot.getFile('main.tex')
  280. expect(mainFile.getHash()).to.be.null
  281. expect(mainFile.getContent()).to.equal('hi world')
  282. return projectId
  283. })
  284. })
  285. // rename the text file
  286. .then(projectId => {
  287. const change = new Change(
  288. [Operation.moveFile('main.tex', 'intro.tex')],
  289. new Date()
  290. )
  291. return basicAuthClient.apis.ProjectImport.importChanges1({
  292. project_id: projectId,
  293. changes: [change.toRaw()],
  294. end_version: 4,
  295. return_snapshot: 'hashed',
  296. }).then(response => {
  297. expect(response.status).to.equal(HTTPStatus.CREATED)
  298. const snapshot = Snapshot.fromRaw(response.obj)
  299. expect(snapshot.countFiles()).to.equal(3)
  300. expect(snapshot.getFile('intro.tex').getHash()).to.equal(
  301. blobHash.fromString('hi world')
  302. )
  303. return projectId
  304. })
  305. })
  306. // get the new project state
  307. .then(projectId => {
  308. return client.apis.Project.getLatestContent({
  309. project_id: projectId,
  310. }).then(response => {
  311. const snapshot = Snapshot.fromRaw(response.obj)
  312. expect(snapshot.countFiles()).to.equal(3)
  313. expect(snapshot.getFile('graph_1.png')).to.exist
  314. expect(snapshot.getFile('graph_2.png')).to.exist
  315. const mainFile = snapshot.getFile('intro.tex')
  316. expect(mainFile.getHash()).to.be.null
  317. expect(mainFile.getContent()).to.equal('hi world')
  318. return projectId
  319. })
  320. })
  321. // remove a graph
  322. .then(projectId => {
  323. const change = new Change(
  324. [Operation.removeFile('graph_1.png')],
  325. new Date()
  326. )
  327. return basicAuthClient.apis.ProjectImport.importChanges1({
  328. project_id: projectId,
  329. changes: [change.toRaw()],
  330. end_version: 5,
  331. return_snapshot: 'hashed',
  332. }).then(response => {
  333. expect(response.status).to.equal(HTTPStatus.CREATED)
  334. const snapshot = Snapshot.fromRaw(response.obj)
  335. expect(snapshot.countFiles()).to.equal(2)
  336. return projectId
  337. })
  338. })
  339. // get the new project state
  340. .then(projectId => {
  341. return client.apis.Project.getLatestContent({
  342. project_id: projectId,
  343. }).then(response => {
  344. const snapshot = Snapshot.fromRaw(response.obj)
  345. expect(snapshot.countFiles()).to.equal(2)
  346. expect(snapshot.getFile('graph_2.png')).to.exist
  347. const mainFile = snapshot.getFile('intro.tex')
  348. expect(mainFile.getHash()).to.be.null
  349. expect(mainFile.getContent()).to.equal('hi world')
  350. return projectId
  351. })
  352. })
  353. // download zip with project content
  354. .then(projectId => {
  355. return downloadZipClient.apis.Project.getZip({
  356. project_id: projectId,
  357. version: 6,
  358. }).then(response => {
  359. expect(response.status).to.equal(HTTPStatus.OK)
  360. const headers = response.headers
  361. expect(headers['content-type']).to.equal('application/octet-stream')
  362. expect(headers['content-disposition']).to.equal(
  363. 'attachment; filename=project.zip'
  364. )
  365. })
  366. })
  367. )
  368. })
  369. })