project_updates.test.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912
  1. const BPromise = require('bluebird')
  2. const { expect } = require('chai')
  3. const fs = BPromise.promisifyAll(require('node:fs'))
  4. const HTTPStatus = require('http-status')
  5. const fetch = require('node-fetch')
  6. const cleanup = require('../storage/support/cleanup')
  7. const fixtures = require('../storage/support/fixtures')
  8. const testFiles = require('../storage/support/test_files')
  9. const expectResponse = require('./support/expect_response')
  10. const testServer = require('./support/test_server')
  11. const core = require('overleaf-editor-core')
  12. const testProjects = require('./support/test_projects')
  13. const Change = core.Change
  14. const ChunkResponse = core.ChunkResponse
  15. const File = core.File
  16. const Operation = core.Operation
  17. const Origin = core.Origin
  18. const Snapshot = core.Snapshot
  19. const TextOperation = core.TextOperation
  20. const V2DocVersions = core.V2DocVersions
  21. const knex = require('../../../../storage').knex
  22. describe('history import', function () {
  23. beforeEach(cleanup.everything)
  24. beforeEach(fixtures.create)
  25. function changeToRaw(change) {
  26. return change.toRaw()
  27. }
  28. function makeChange(operation) {
  29. return new Change([operation], new Date(), [])
  30. }
  31. let basicAuthClient
  32. let pseudoJwtBasicAuthClient
  33. let clientForProject
  34. before(async function () {
  35. basicAuthClient = testServer.basicAuthClient
  36. pseudoJwtBasicAuthClient = testServer.pseudoJwtBasicAuthClient
  37. clientForProject = await testServer.createClientForProject('1')
  38. })
  39. it('creates blobs and then imports a snapshot and history', function () {
  40. // We need to be able to set the projectId to match an existing doc ID.
  41. const testProjectId = '1'
  42. const testFilePathname = 'main.tex'
  43. const testAuthors = [123, null]
  44. const testTextOperation0 = TextOperation.fromJSON({ textOperation: ['a'] })
  45. const testTextOperation1 = TextOperation.fromJSON({
  46. textOperation: [1, 'b'],
  47. })
  48. let testSnapshot
  49. return fetch(
  50. testServer.url(
  51. `/api/projects/${testProjectId}/blobs/${File.EMPTY_FILE_HASH}`
  52. ),
  53. {
  54. method: 'PUT',
  55. body: fs.createReadStream(testFiles.path('empty.tex')),
  56. headers: {
  57. Authorization: testServer.basicAuthHeader,
  58. },
  59. }
  60. )
  61. .then(response => {
  62. expect(response.ok).to.be.true
  63. })
  64. .then(() => {
  65. // Import project
  66. testSnapshot = new Snapshot()
  67. testSnapshot.addFile(
  68. testFilePathname,
  69. File.fromHash(File.EMPTY_FILE_HASH)
  70. )
  71. return basicAuthClient.apis.ProjectImport.importSnapshot1({
  72. project_id: testProjectId,
  73. snapshot: testSnapshot.toRaw(),
  74. })
  75. })
  76. .then(response => {
  77. // Check project is valid
  78. expect(response.obj.projectId).to.equal(testProjectId)
  79. })
  80. .then(() => {
  81. // Try importing the project again
  82. return basicAuthClient.apis.ProjectImport.importSnapshot1({
  83. project_id: testProjectId,
  84. snapshot: testSnapshot.toRaw(),
  85. })
  86. })
  87. .then(() => {
  88. // Check that importing a duplicate fails
  89. expect.fail()
  90. })
  91. .catch(expectResponse.conflict)
  92. .then(() => {
  93. // Get project history
  94. return clientForProject.apis.Project.getLatestHistory({
  95. project_id: testProjectId,
  96. })
  97. })
  98. .then(response => {
  99. // Check that the imported history is valid
  100. const chunk = ChunkResponse.fromRaw(response.obj).getChunk()
  101. const snapshot = chunk.getSnapshot()
  102. expect(snapshot.countFiles()).to.equal(1)
  103. const file = snapshot.getFile(testFilePathname)
  104. expect(file.getHash()).to.eql(File.EMPTY_FILE_HASH)
  105. expect(chunk.getChanges().length).to.equal(0)
  106. expect(chunk.getEndVersion()).to.equal(0)
  107. })
  108. .then(() => {
  109. // Import changes with an end version
  110. const changes = [
  111. makeChange(Operation.editFile(testFilePathname, testTextOperation0)),
  112. makeChange(Operation.editFile(testFilePathname, testTextOperation1)),
  113. ]
  114. changes[0].setAuthors(testAuthors)
  115. return basicAuthClient.apis.ProjectImport.importChanges1({
  116. project_id: testProjectId,
  117. changes: changes.map(changeToRaw),
  118. end_version: 0,
  119. return_snapshot: 'hashed',
  120. })
  121. })
  122. .then(response => {
  123. expect(response.status).to.equal(HTTPStatus.CREATED)
  124. const snapshot = Snapshot.fromRaw(response.obj)
  125. expect(snapshot.countFiles()).to.equal(1)
  126. expect(snapshot.getFile('main.tex').getHash()).to.equal(
  127. testFiles.STRING_AB_HASH
  128. )
  129. })
  130. .then(() => {
  131. // Get project history
  132. return clientForProject.apis.Project.getLatestHistory({
  133. project_id: testProjectId,
  134. })
  135. })
  136. .then(response => {
  137. // Check that the history is valid
  138. const chunkResponse = ChunkResponse.fromRaw(response.obj)
  139. const chunk = chunkResponse.getChunk()
  140. const snapshot = chunk.getSnapshot()
  141. expect(snapshot.countFiles()).to.equal(1)
  142. const file = snapshot.getFile(testFilePathname)
  143. expect(file.getHash()).to.equal(File.EMPTY_FILE_HASH)
  144. expect(chunk.getChanges().length).to.equal(2)
  145. const changeWithAuthors = chunk.getChanges()[0]
  146. expect(changeWithAuthors.getAuthors().length).to.equal(2)
  147. expect(changeWithAuthors.getAuthors()).to.deep.equal(testAuthors)
  148. expect(chunk.getStartVersion()).to.equal(0)
  149. expect(chunk.getEndVersion()).to.equal(2)
  150. })
  151. .then(() => {
  152. return clientForProject.apis.Project.getLatestHistory({
  153. project_id: testProjectId,
  154. })
  155. })
  156. .then(response => {
  157. // it should retrieve the same chunk
  158. const chunkResponse = ChunkResponse.fromRaw(response.obj)
  159. const chunk = chunkResponse.getChunk()
  160. expect(chunk.getChanges().length).to.equal(2)
  161. expect(chunk.getStartVersion()).to.equal(0)
  162. expect(chunk.getEndVersion()).to.equal(2)
  163. })
  164. .then(() => {
  165. // Get project's latest content
  166. return clientForProject.apis.Project.getLatestContent({
  167. project_id: testProjectId,
  168. })
  169. })
  170. .then(response => {
  171. // Check that the content is valid
  172. const snapshot = Snapshot.fromRaw(response.obj)
  173. expect(snapshot.countFiles()).to.equal(1)
  174. const file = snapshot.getFile(testFilePathname)
  175. expect(file.getContent()).to.equal('ab')
  176. })
  177. })
  178. it('rejects invalid changes in history', function () {
  179. const testProjectId = '1'
  180. const testFilePathname = 'main.tex'
  181. const testTextOperation = TextOperation.fromJSON({
  182. textOperation: ['a', 10],
  183. })
  184. let testSnapshot
  185. return fetch(
  186. testServer.url(
  187. `/api/projects/${testProjectId}/blobs/${File.EMPTY_FILE_HASH}`
  188. ),
  189. {
  190. method: 'PUT',
  191. body: fs.createReadStream(testFiles.path('empty.tex')),
  192. headers: {
  193. Authorization: testServer.basicAuthHeader,
  194. },
  195. }
  196. )
  197. .then(response => {
  198. expect(response.ok).to.be.true
  199. })
  200. .then(() => {
  201. // Import project
  202. testSnapshot = new Snapshot()
  203. testSnapshot.addFile(
  204. testFilePathname,
  205. File.fromHash(File.EMPTY_FILE_HASH)
  206. )
  207. return basicAuthClient.apis.ProjectImport.importSnapshot1({
  208. project_id: testProjectId,
  209. snapshot: testSnapshot.toRaw(),
  210. })
  211. })
  212. .then(response => {
  213. // Check project is valid
  214. expect(response.obj.projectId).to.equal(testProjectId)
  215. })
  216. .then(() => {
  217. // Import invalid changes
  218. const changes = [
  219. makeChange(Operation.editFile(testFilePathname, testTextOperation)),
  220. ]
  221. return basicAuthClient.apis.ProjectImport.importChanges1({
  222. project_id: testProjectId,
  223. end_version: 0,
  224. return_snapshot: 'hashed',
  225. changes: changes.map(changeToRaw),
  226. })
  227. })
  228. .then(() => {
  229. // Check that this fails
  230. expect.fail()
  231. })
  232. .catch(expectResponse.unprocessableEntity)
  233. .then(() => {
  234. // Get the latest content
  235. return clientForProject.apis.Project.getLatestContent({
  236. project_id: testProjectId,
  237. })
  238. })
  239. .then(response => {
  240. // Check that no changes have been stored
  241. const snapshot = Snapshot.fromRaw(response.obj)
  242. expect(snapshot.countFiles()).to.equal(1)
  243. const file = snapshot.getFile(testFilePathname)
  244. expect(file.getContent()).to.equal('')
  245. })
  246. .then(() => {
  247. // Send a change with the wrong end version that is not conflicting
  248. // with the latest snapshot
  249. const changes = [makeChange(Operation.removeFile(testFilePathname))]
  250. return basicAuthClient.apis.ProjectImport.importChanges1({
  251. project_id: testProjectId,
  252. end_version: 10000,
  253. changes,
  254. })
  255. })
  256. .then(() => {
  257. // Check that this fails
  258. expect.fail()
  259. })
  260. .catch(expectResponse.unprocessableEntity)
  261. .then(() => {
  262. // Get the latest project history
  263. return clientForProject.apis.Project.getLatestHistory({
  264. project_id: testProjectId,
  265. })
  266. })
  267. .then(response => {
  268. // Check that no changes have been stored
  269. const chunkResponse = ChunkResponse.fromRaw(response.obj)
  270. const changes = chunkResponse.getChunk().getChanges()
  271. expect(changes).to.have.length(0)
  272. })
  273. })
  274. it('creates and edits a file using changes', function () {
  275. const testProjectId = '1'
  276. const mainFilePathname = 'main.tex'
  277. const testFilePathname = 'test.tex'
  278. const testTextOperation = TextOperation.fromJSON({ textOperation: ['a'] })
  279. const inexistentAuthors = [1234, 5678]
  280. const projectVersion = '12345.0'
  281. const v2DocVersions = new V2DocVersions({
  282. 'random-doc-id': { pathname: 'doc-path.tex', v: 123 },
  283. })
  284. const testLabelOrigin = Origin.fromRaw({
  285. kind: 'saved ver',
  286. })
  287. const testRestoreOrigin = Origin.fromRaw({
  288. kind: 'restore',
  289. timestamp: '2016-01-01T00:00:00',
  290. version: 1,
  291. })
  292. let testSnapshot
  293. return fetch(
  294. testServer.url(
  295. `/api/projects/${testProjectId}/blobs/${File.EMPTY_FILE_HASH}`
  296. ),
  297. {
  298. method: 'PUT',
  299. body: fs.createReadStream(testFiles.path('empty.tex')),
  300. headers: {
  301. Authorization: testServer.basicAuthHeader,
  302. },
  303. }
  304. )
  305. .then(response => {
  306. expect(response.ok).to.be.true
  307. })
  308. .then(() => {
  309. // Import a project
  310. testSnapshot = new Snapshot()
  311. testSnapshot.addFile(
  312. mainFilePathname,
  313. File.fromHash(File.EMPTY_FILE_HASH)
  314. )
  315. return basicAuthClient.apis.ProjectImport.importSnapshot1({
  316. project_id: testProjectId,
  317. snapshot: testSnapshot.toRaw(),
  318. })
  319. })
  320. .then(response => {
  321. // Check that the project is valid
  322. expect(response.obj.projectId).to.equal(testProjectId)
  323. })
  324. .then(() => {
  325. // Import changes
  326. const testFile = File.fromHash(File.EMPTY_FILE_HASH)
  327. const changes = [
  328. makeChange(Operation.addFile(testFilePathname, testFile)),
  329. makeChange(Operation.editFile(testFilePathname, testTextOperation)),
  330. ]
  331. changes[0].setProjectVersion(projectVersion)
  332. changes[1].setAuthors(inexistentAuthors)
  333. changes[1].setV2DocVersions(v2DocVersions)
  334. return basicAuthClient.apis.ProjectImport.importChanges1({
  335. project_id: testProjectId,
  336. end_version: 0,
  337. return_snapshot: 'hashed',
  338. changes: changes.map(changeToRaw),
  339. })
  340. })
  341. .then(response => {
  342. expect(response.status).to.equal(HTTPStatus.CREATED)
  343. const snapshot = Snapshot.fromRaw(response.obj)
  344. expect(snapshot.countFiles()).to.equal(2)
  345. expect(snapshot.getFile('main.tex').getHash()).to.equal(
  346. File.EMPTY_FILE_HASH
  347. )
  348. expect(snapshot.getFile('test.tex').getHash()).to.equal(
  349. testFiles.STRING_A_HASH
  350. )
  351. })
  352. .then(() => {
  353. // Get the project history
  354. return clientForProject.apis.Project.getLatestHistory({
  355. project_id: testProjectId,
  356. })
  357. })
  358. .then(response => {
  359. // it should not fail when the some of the authors do not exist anymore
  360. const chunkResponse = ChunkResponse.fromRaw(response.obj)
  361. const changes = chunkResponse.getChunk().getChanges()
  362. expect(changes.length).to.equal(2)
  363. const changeWithAuthor = changes[1]
  364. expect(changeWithAuthor.getAuthors()).to.deep.equal(inexistentAuthors)
  365. })
  366. .then(() => {
  367. // it should retrieve the latest snapshot when the changes set is empty
  368. return basicAuthClient.apis.ProjectImport.importChanges1({
  369. project_id: testProjectId,
  370. end_version: 0,
  371. return_snapshot: 'hashed',
  372. changes: [],
  373. })
  374. })
  375. .then(response => {
  376. // Check latest snapshot
  377. const snapshot = Snapshot.fromRaw(response.obj)
  378. expect(snapshot.countFiles()).to.equal(2)
  379. expect(snapshot.getFile('main.tex').getHash()).to.equal(
  380. File.EMPTY_FILE_HASH
  381. )
  382. expect(snapshot.getFile('test.tex').getHash()).to.equal(
  383. testFiles.STRING_A_HASH
  384. )
  385. expect(snapshot.getProjectVersion()).to.equal(projectVersion)
  386. expect(snapshot.getV2DocVersions()).to.deep.equal(v2DocVersions)
  387. })
  388. .then(() => {
  389. // Import changes with origin
  390. const testFile = File.fromHash(File.EMPTY_FILE_HASH)
  391. const changes = [
  392. makeChange(Operation.removeFile(testFilePathname)),
  393. makeChange(Operation.addFile(testFilePathname, testFile)),
  394. ]
  395. changes[0].setOrigin(testLabelOrigin)
  396. changes[1].setOrigin(testRestoreOrigin)
  397. return basicAuthClient.apis.ProjectImport.importChanges1({
  398. project_id: testProjectId,
  399. end_version: 0,
  400. changes: changes.map(changeToRaw),
  401. })
  402. })
  403. .then(() => {
  404. // Get the latest history
  405. return clientForProject.apis.Project.getLatestHistory({
  406. project_id: testProjectId,
  407. })
  408. })
  409. .then(response => {
  410. // Check that the origin is stored
  411. const chunkResponse = ChunkResponse.fromRaw(response.obj)
  412. const changes = chunkResponse.getChunk().getChanges()
  413. expect(changes).to.have.length(4)
  414. expect(changes[2].getOrigin()).to.deep.equal(testLabelOrigin)
  415. expect(changes[3].getOrigin()).to.deep.equal(testRestoreOrigin)
  416. })
  417. .then(() => {
  418. // Import invalid changes
  419. const testFile = File.fromHash(File.EMPTY_FILE_HASH)
  420. const changes = [makeChange(Operation.addFile('../../a.tex', testFile))]
  421. return basicAuthClient.apis.ProjectImport.importChanges1({
  422. project_id: testProjectId,
  423. end_version: 0,
  424. changes: changes.map(changeToRaw),
  425. })
  426. })
  427. .then(() => {
  428. // Check that this fails and returns a 422
  429. expect.fail()
  430. })
  431. .catch(expectResponse.unprocessableEntity)
  432. })
  433. it('imports changes with git-bridge origin', function () {
  434. const testProjectId = '1'
  435. const testFilePathname = 'git.tex'
  436. const testFile = File.fromHash(File.EMPTY_FILE_HASH)
  437. const testGitOrigin = Origin.fromRaw({
  438. kind: 'git-bridge',
  439. })
  440. let testSnapshot
  441. return fetch(
  442. testServer.url(
  443. `/api/projects/${testProjectId}/blobs/${File.EMPTY_FILE_HASH}`
  444. ),
  445. {
  446. method: 'PUT',
  447. body: fs.createReadStream(testFiles.path('empty.tex')),
  448. headers: {
  449. Authorization: testServer.basicAuthHeader,
  450. },
  451. }
  452. )
  453. .then(response => {
  454. expect(response.ok).to.be.true
  455. })
  456. .then(() => {
  457. testSnapshot = new Snapshot()
  458. testSnapshot.addFile(testFilePathname, testFile)
  459. return basicAuthClient.apis.ProjectImport.importSnapshot1({
  460. project_id: testProjectId,
  461. snapshot: testSnapshot.toRaw(),
  462. })
  463. })
  464. .then(response => {
  465. expect(response.obj.projectId).to.equal(testProjectId)
  466. })
  467. .then(() => {
  468. const changes = [
  469. makeChange(Operation.addFile(testFilePathname, testFile)),
  470. ]
  471. changes[0].setOrigin(testGitOrigin)
  472. return basicAuthClient.apis.ProjectImport.importChanges1({
  473. project_id: testProjectId,
  474. end_version: 0,
  475. changes: changes.map(changeToRaw),
  476. })
  477. })
  478. .then(response => {
  479. expect(response.status).to.equal(HTTPStatus.CREATED)
  480. })
  481. .then(() => {
  482. return clientForProject.apis.Project.getLatestHistory({
  483. project_id: testProjectId,
  484. })
  485. })
  486. .then(response => {
  487. const chunkResponse = ChunkResponse.fromRaw(response.obj)
  488. const changes = chunkResponse.getChunk().getChanges()
  489. expect(changes.length).to.be.at.least(1)
  490. const lastChange = changes[changes.length - 1]
  491. expect(lastChange.getOrigin()).to.deep.equal(testGitOrigin)
  492. })
  493. })
  494. it('rejects text operations on binary files', function () {
  495. const testProjectId = '1'
  496. const testFilePathname = 'main.tex'
  497. const testTextOperation = TextOperation.fromJSON({ textOperation: ['bb'] })
  498. let testSnapshot
  499. return fetch(
  500. testServer.url(
  501. `/api/projects/${testProjectId}/blobs/${testFiles.NON_BMP_TXT_HASH}`
  502. ),
  503. {
  504. method: 'PUT',
  505. body: fs.createReadStream(testFiles.path('non_bmp.txt')),
  506. headers: {
  507. Authorization: testServer.basicAuthHeader,
  508. },
  509. }
  510. )
  511. .then(response => {
  512. expect(response.ok).to.be.true
  513. })
  514. .then(() => {
  515. // Import a project
  516. testSnapshot = new Snapshot()
  517. testSnapshot.addFile(
  518. testFilePathname,
  519. File.fromHash(testFiles.NON_BMP_TXT_HASH)
  520. )
  521. return basicAuthClient.apis.ProjectImport.importSnapshot1({
  522. project_id: testProjectId,
  523. snapshot: testSnapshot.toRaw(),
  524. })
  525. })
  526. .then(response => {
  527. // Check that the project is valid
  528. expect(response.obj.projectId).to.equal(testProjectId)
  529. })
  530. .then(() => {
  531. // Import invalid changes
  532. const changes = [
  533. makeChange(Operation.editFile(testFilePathname, testTextOperation)),
  534. ]
  535. return basicAuthClient.apis.ProjectImport.importChanges1({
  536. project_id: testProjectId,
  537. end_version: 0,
  538. changes: changes.map(changeToRaw),
  539. })
  540. })
  541. .then(() => {
  542. // Expect invalid changes to fail
  543. expect.fail()
  544. })
  545. .catch(expectResponse.unprocessableEntity)
  546. .then(() => {
  547. // Get latest content
  548. return clientForProject.apis.Project.getLatestContent({
  549. project_id: testProjectId,
  550. })
  551. })
  552. .then(response => {
  553. // Check that no changes were stored
  554. const snapshot = Snapshot.fromRaw(response.obj)
  555. expect(snapshot.countFiles()).to.equal(1)
  556. expect(snapshot.getFile(testFilePathname).getHash()).to.equal(
  557. testFiles.NON_BMP_TXT_HASH
  558. )
  559. })
  560. })
  561. it('accepts text operation on files with null characters if stringLength is present', function () {
  562. const testProjectId = '1'
  563. const mainFilePathname = 'main.tex'
  564. const testTextOperation = TextOperation.fromJSON({
  565. textOperation: [3, 'a'],
  566. })
  567. let testSnapshot
  568. function importChanges() {
  569. const changes = [
  570. makeChange(Operation.editFile(mainFilePathname, testTextOperation)),
  571. ]
  572. return basicAuthClient.apis.ProjectImport.importChanges1({
  573. project_id: testProjectId,
  574. end_version: 0,
  575. changes: changes.map(changeToRaw),
  576. })
  577. }
  578. function getLatestContent() {
  579. return clientForProject.apis.Project.getLatestContent({
  580. project_id: testProjectId,
  581. })
  582. }
  583. return fetch(
  584. testServer.url(
  585. `/api/projects/${testProjectId}/blobs/${testFiles.NULL_CHARACTERS_TXT_HASH}`
  586. ),
  587. {
  588. method: 'PUT',
  589. body: fs.createReadStream(testFiles.path('null_characters.txt')),
  590. headers: {
  591. Authorization: testServer.basicAuthHeader,
  592. },
  593. }
  594. )
  595. .then(response => {
  596. expect(response.ok).to.be.true
  597. })
  598. .then(() => {
  599. // Import project
  600. testSnapshot = new Snapshot()
  601. testSnapshot.addFile(
  602. mainFilePathname,
  603. File.fromHash(testFiles.NULL_CHARACTERS_TXT_HASH)
  604. )
  605. return basicAuthClient.apis.ProjectImport.importSnapshot1({
  606. project_id: testProjectId,
  607. snapshot: testSnapshot.toRaw(),
  608. })
  609. })
  610. .then(importChanges)
  611. .then(() => {
  612. // Expect invalid changes to fail
  613. expect.fail()
  614. })
  615. .catch(expectResponse.unprocessableEntity)
  616. .then(getLatestContent)
  617. .then(response => {
  618. // Check that no changes were made
  619. const snapshot = Snapshot.fromRaw(response.obj)
  620. expect(snapshot.countFiles()).to.equal(1)
  621. expect(snapshot.getFile(mainFilePathname).getHash()).to.equal(
  622. testFiles.NULL_CHARACTERS_TXT_HASH
  623. )
  624. })
  625. .then(() => {
  626. // Set string length
  627. return knex('project_blobs').update(
  628. 'string_length',
  629. testFiles.NULL_CHARACTERS_TXT_BYTE_LENGTH
  630. )
  631. })
  632. .then(importChanges)
  633. .then(getLatestContent)
  634. .then(response => {
  635. const snapshot = Snapshot.fromRaw(response.obj)
  636. expect(snapshot.countFiles()).to.equal(1)
  637. expect(snapshot.getFile(mainFilePathname).getContent()).to.equal(
  638. '\x00\x00\x00a'
  639. )
  640. })
  641. })
  642. it('returns 404 when chunk is not found in bucket', function () {
  643. const testProjectId = '1'
  644. const fooChange = makeChange(Operation.removeFile('foo.tex'))
  645. return knex('chunks')
  646. .insert({
  647. doc_id: testProjectId,
  648. start_version: 0,
  649. end_version: 100,
  650. end_timestamp: null,
  651. })
  652. .then(() => {
  653. // Import changes
  654. return basicAuthClient.apis.ProjectImport.importChanges1({
  655. project_id: testProjectId,
  656. end_version: 100,
  657. changes: [fooChange.toRaw()],
  658. })
  659. })
  660. .then(() => {
  661. // Expect invalid changes to fail
  662. expect.fail()
  663. })
  664. .catch(expectResponse.notFound)
  665. })
  666. it('creates and returns changes with v2 author ids', function () {
  667. const testFilePathname = 'test.tex'
  668. const testTextOperation = TextOperation.fromJSON({ textOperation: ['a'] })
  669. const v2Authors = ['5a296963ad5e82432674c839', null]
  670. let testProjectId
  671. return testProjects
  672. .createEmptyProject()
  673. .then(projectId => {
  674. testProjectId = projectId
  675. expect(testProjectId).to.be.a('string')
  676. })
  677. .then(() => {
  678. return fetch(
  679. testServer.url(
  680. `/api/projects/${testProjectId}/blobs/${File.EMPTY_FILE_HASH}`
  681. ),
  682. {
  683. method: 'PUT',
  684. body: fs.createReadStream(testFiles.path('empty.tex')),
  685. headers: {
  686. Authorization: testServer.basicAuthHeader,
  687. },
  688. }
  689. )
  690. })
  691. .then(response => {
  692. expect(response.ok).to.be.true
  693. })
  694. .then(() => {
  695. // Import changes
  696. const testFile = File.fromHash(File.EMPTY_FILE_HASH)
  697. const changes = [
  698. makeChange(Operation.addFile(testFilePathname, testFile)),
  699. makeChange(Operation.editFile(testFilePathname, testTextOperation)),
  700. ]
  701. changes[1].setV2Authors(v2Authors)
  702. return basicAuthClient.apis.ProjectImport.importChanges1({
  703. project_id: testProjectId,
  704. end_version: 0,
  705. return_snapshot: 'hashed',
  706. changes: changes.map(changeToRaw),
  707. })
  708. })
  709. .then(response => {
  710. expect(response.status).to.equal(HTTPStatus.CREATED)
  711. const snapshot = Snapshot.fromRaw(response.obj)
  712. expect(snapshot.countFiles()).to.equal(1)
  713. expect(snapshot.getFile('test.tex').getHash()).to.equal(
  714. testFiles.STRING_A_HASH
  715. )
  716. })
  717. .then(() => {
  718. // Get project history
  719. return pseudoJwtBasicAuthClient.apis.Project.getLatestHistory({
  720. project_id: testProjectId,
  721. })
  722. })
  723. .then(response => {
  724. // it should not fail when the some of the authors do not exist anymore
  725. const chunkResponse = ChunkResponse.fromRaw(response.obj)
  726. const changes = chunkResponse.getChunk().getChanges()
  727. expect(changes.length).to.equal(2)
  728. const changeWithAuthor = changes[1]
  729. expect(changeWithAuthor.getV2Authors()).to.deep.equal(v2Authors)
  730. })
  731. })
  732. it('should reject invalid v2 author ids', function () {
  733. const testFilePathname = 'test.tex'
  734. const v2Authors = ['not-a-v2-id']
  735. let testProjectId
  736. return testProjects
  737. .createEmptyProject()
  738. .then(projectId => {
  739. testProjectId = projectId
  740. expect(testProjectId).to.be.a('string')
  741. })
  742. .then(() => {
  743. return fetch(
  744. testServer.url(
  745. `/api/projects/${testProjectId}/blobs/${File.EMPTY_FILE_HASH}`
  746. ),
  747. {
  748. method: 'PUT',
  749. body: fs.createReadStream(testFiles.path('empty.tex')),
  750. headers: {
  751. Authorization: testServer.basicAuthHeader,
  752. },
  753. }
  754. )
  755. })
  756. .then(response => {
  757. expect(response.ok).to.be.true
  758. })
  759. .then(() => {
  760. // Import changes
  761. const testFile = File.fromHash(File.EMPTY_FILE_HASH)
  762. const changes = [
  763. makeChange(Operation.addFile(testFilePathname, testFile)),
  764. ]
  765. changes[0].v2Authors = v2Authors
  766. return basicAuthClient.apis.ProjectImport.importChanges1({
  767. project_id: testProjectId,
  768. end_version: 0,
  769. changes: changes.map(changeToRaw),
  770. })
  771. })
  772. .then(() => {
  773. // Check that invalid changes fail
  774. expect.fail()
  775. })
  776. .catch(expectResponse.unprocessableEntity)
  777. })
  778. it('should reject changes with both v1 and v2 authors ids', function () {
  779. const testFilePathname = 'test.tex'
  780. const v1Authors = [456]
  781. const v2Authors = ['5a296963ad5e82432674c839', null]
  782. let testProjectId
  783. return testProjects
  784. .createEmptyProject()
  785. .then(projectId => {
  786. testProjectId = projectId
  787. expect(testProjectId).to.be.a('string')
  788. })
  789. .then(() => {
  790. return fetch(
  791. testServer.url(
  792. `/api/projects/${testProjectId}/blobs/${File.EMPTY_FILE_HASH}`
  793. ),
  794. {
  795. method: 'PUT',
  796. body: fs.createReadStream(testFiles.path('empty.tex')),
  797. headers: {
  798. Authorization: testServer.basicAuthHeader,
  799. },
  800. }
  801. )
  802. })
  803. .then(response => {
  804. expect(response.ok).to.be.true
  805. })
  806. .then(() => {
  807. // Import changes
  808. const testFile = File.fromHash(File.EMPTY_FILE_HASH)
  809. const changes = [
  810. makeChange(Operation.addFile(testFilePathname, testFile)),
  811. ]
  812. changes[0].authors = v1Authors
  813. changes[0].v2Authors = v2Authors
  814. return basicAuthClient.apis.ProjectImport.importChanges1({
  815. project_id: testProjectId,
  816. end_version: 0,
  817. changes: changes.map(changeToRaw),
  818. })
  819. })
  820. .then(() => {
  821. // Check that invalid changes fail
  822. expect.fail()
  823. })
  824. .catch(expectResponse.unprocessableEntity)
  825. })
  826. it("returns unprocessable if end_version isn't provided", function () {
  827. return testProjects
  828. .createEmptyProject()
  829. .then(projectId => {
  830. expect(projectId).to.be.a('string')
  831. return projectId
  832. })
  833. .then(projectId => {
  834. // Import changes
  835. return basicAuthClient.apis.ProjectImport.importChanges1({
  836. project_id: projectId,
  837. changes: [],
  838. })
  839. })
  840. .then(() => {
  841. // Check that invalid changes fail
  842. expect.fail()
  843. })
  844. .catch(error => {
  845. expect(error.message).to.equal('request failed with status 422')
  846. })
  847. })
  848. it('returns unprocessable if return_snapshot is invalid', function () {
  849. return testProjects
  850. .createEmptyProject()
  851. .then(projectId => {
  852. // Import changes
  853. return basicAuthClient.apis.ProjectImport.importChanges1({
  854. project_id: projectId,
  855. changes: [],
  856. end_version: 0,
  857. return_snapshot: 'not_a_valid_value',
  858. })
  859. })
  860. .then(() => {
  861. // Check that invalid changes fail
  862. expect.fail()
  863. })
  864. .catch(error => {
  865. expect(error.status).to.equal(HTTPStatus.UNPROCESSABLE_ENTITY)
  866. })
  867. })
  868. })