end_to_end.test.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  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('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. [Operation.editFile('main.tex', TextOperation.fromJSON(['hello']))],
  197. new Date()
  198. )
  199. return basicAuthClient.apis.ProjectImport.importChanges1({
  200. project_id: projectId,
  201. changes: [change.toRaw()],
  202. end_version: 2,
  203. return_snapshot: 'hashed',
  204. }).then(response => {
  205. expect(response.status).to.equal(HTTPStatus.CREATED)
  206. const snapshot = Snapshot.fromRaw(response.obj)
  207. expect(snapshot.countFiles()).to.equal(3)
  208. expect(snapshot.getFile('graph_1.png').getHash()).to.equal(
  209. testFiles.GRAPH_PNG_HASH
  210. )
  211. expect(snapshot.getFile('graph_2.png').getHash()).to.equal(
  212. testFiles.GRAPH_PNG_HASH
  213. )
  214. expect(snapshot.getFile('main.tex').getHash()).to.equal(
  215. blobHash.fromString('hello')
  216. )
  217. return projectId
  218. })
  219. })
  220. // get the new project state
  221. .then(projectId => {
  222. return client.apis.Project.getLatestContent({
  223. project_id: projectId,
  224. }).then(response => {
  225. const snapshot = Snapshot.fromRaw(response.obj)
  226. expect(snapshot.countFiles()).to.equal(3)
  227. expect(snapshot.getFile('graph_1.png').getHash()).to.equal(
  228. testFiles.GRAPH_PNG_HASH
  229. )
  230. expect(snapshot.getFile('graph_2.png').getHash()).to.equal(
  231. testFiles.GRAPH_PNG_HASH
  232. )
  233. const mainFile = snapshot.getFile('main.tex')
  234. expect(mainFile.getHash()).to.be.null
  235. expect(mainFile.getContent()).to.equal('hello')
  236. return projectId
  237. })
  238. })
  239. // edit the main file again
  240. .then(projectId => {
  241. const change = new Change(
  242. [
  243. Operation.editFile(
  244. 'main.tex',
  245. TextOperation.fromJSON([1, -4, 'i world'])
  246. ),
  247. ],
  248. new Date()
  249. )
  250. return basicAuthClient.apis.ProjectImport.importChanges1({
  251. project_id: projectId,
  252. changes: [change.toRaw()],
  253. end_version: 3,
  254. return_snapshot: 'hashed',
  255. }).then(response => {
  256. expect(response.status).to.equal(HTTPStatus.CREATED)
  257. const snapshot = Snapshot.fromRaw(response.obj)
  258. expect(snapshot.countFiles()).to.equal(3)
  259. expect(snapshot.getFile('main.tex').getHash()).to.equal(
  260. blobHash.fromString('hi world')
  261. )
  262. return projectId
  263. })
  264. })
  265. // get the new project state
  266. .then(projectId => {
  267. return client.apis.Project.getLatestContent({
  268. project_id: projectId,
  269. }).then(response => {
  270. const snapshot = Snapshot.fromRaw(response.obj)
  271. expect(snapshot.countFiles()).to.equal(3)
  272. expect(snapshot.getFile('graph_1.png')).to.exist
  273. expect(snapshot.getFile('graph_2.png')).to.exist
  274. const mainFile = snapshot.getFile('main.tex')
  275. expect(mainFile.getHash()).to.be.null
  276. expect(mainFile.getContent()).to.equal('hi world')
  277. return projectId
  278. })
  279. })
  280. // rename the text file
  281. .then(projectId => {
  282. const change = new Change(
  283. [Operation.moveFile('main.tex', 'intro.tex')],
  284. new Date()
  285. )
  286. return basicAuthClient.apis.ProjectImport.importChanges1({
  287. project_id: projectId,
  288. changes: [change.toRaw()],
  289. end_version: 4,
  290. return_snapshot: 'hashed',
  291. }).then(response => {
  292. expect(response.status).to.equal(HTTPStatus.CREATED)
  293. const snapshot = Snapshot.fromRaw(response.obj)
  294. expect(snapshot.countFiles()).to.equal(3)
  295. expect(snapshot.getFile('intro.tex').getHash()).to.equal(
  296. blobHash.fromString('hi world')
  297. )
  298. return projectId
  299. })
  300. })
  301. // get the new project state
  302. .then(projectId => {
  303. return client.apis.Project.getLatestContent({
  304. project_id: projectId,
  305. }).then(response => {
  306. const snapshot = Snapshot.fromRaw(response.obj)
  307. expect(snapshot.countFiles()).to.equal(3)
  308. expect(snapshot.getFile('graph_1.png')).to.exist
  309. expect(snapshot.getFile('graph_2.png')).to.exist
  310. const mainFile = snapshot.getFile('intro.tex')
  311. expect(mainFile.getHash()).to.be.null
  312. expect(mainFile.getContent()).to.equal('hi world')
  313. return projectId
  314. })
  315. })
  316. // remove a graph
  317. .then(projectId => {
  318. const change = new Change(
  319. [Operation.removeFile('graph_1.png')],
  320. new Date()
  321. )
  322. return basicAuthClient.apis.ProjectImport.importChanges1({
  323. project_id: projectId,
  324. changes: [change.toRaw()],
  325. end_version: 5,
  326. return_snapshot: 'hashed',
  327. }).then(response => {
  328. expect(response.status).to.equal(HTTPStatus.CREATED)
  329. const snapshot = Snapshot.fromRaw(response.obj)
  330. expect(snapshot.countFiles()).to.equal(2)
  331. return projectId
  332. })
  333. })
  334. // get the new project state
  335. .then(projectId => {
  336. return client.apis.Project.getLatestContent({
  337. project_id: projectId,
  338. }).then(response => {
  339. const snapshot = Snapshot.fromRaw(response.obj)
  340. expect(snapshot.countFiles()).to.equal(2)
  341. expect(snapshot.getFile('graph_2.png')).to.exist
  342. const mainFile = snapshot.getFile('intro.tex')
  343. expect(mainFile.getHash()).to.be.null
  344. expect(mainFile.getContent()).to.equal('hi world')
  345. return projectId
  346. })
  347. })
  348. // download zip with project content
  349. .then(projectId => {
  350. return downloadZipClient.apis.Project.getZip({
  351. project_id: projectId,
  352. version: 6,
  353. }).then(response => {
  354. expect(response.status).to.equal(HTTPStatus.OK)
  355. const headers = response.headers
  356. expect(headers['content-type']).to.equal('application/octet-stream')
  357. expect(headers['content-disposition']).to.equal(
  358. 'attachment; filename=project.zip'
  359. )
  360. })
  361. })
  362. )
  363. })
  364. })