project_updates.test.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853
  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('rejects text operations on binary files', function () {
  434. const testProjectId = '1'
  435. const testFilePathname = 'main.tex'
  436. const testTextOperation = TextOperation.fromJSON({ textOperation: ['bb'] })
  437. let testSnapshot
  438. return fetch(
  439. testServer.url(
  440. `/api/projects/${testProjectId}/blobs/${testFiles.NON_BMP_TXT_HASH}`
  441. ),
  442. {
  443. method: 'PUT',
  444. body: fs.createReadStream(testFiles.path('non_bmp.txt')),
  445. headers: {
  446. Authorization: testServer.basicAuthHeader,
  447. },
  448. }
  449. )
  450. .then(response => {
  451. expect(response.ok).to.be.true
  452. })
  453. .then(() => {
  454. // Import a project
  455. testSnapshot = new Snapshot()
  456. testSnapshot.addFile(
  457. testFilePathname,
  458. File.fromHash(testFiles.NON_BMP_TXT_HASH)
  459. )
  460. return basicAuthClient.apis.ProjectImport.importSnapshot1({
  461. project_id: testProjectId,
  462. snapshot: testSnapshot.toRaw(),
  463. })
  464. })
  465. .then(response => {
  466. // Check that the project is valid
  467. expect(response.obj.projectId).to.equal(testProjectId)
  468. })
  469. .then(() => {
  470. // Import invalid changes
  471. const changes = [
  472. makeChange(Operation.editFile(testFilePathname, testTextOperation)),
  473. ]
  474. return basicAuthClient.apis.ProjectImport.importChanges1({
  475. project_id: testProjectId,
  476. end_version: 0,
  477. changes: changes.map(changeToRaw),
  478. })
  479. })
  480. .then(() => {
  481. // Expect invalid changes to fail
  482. expect.fail()
  483. })
  484. .catch(expectResponse.unprocessableEntity)
  485. .then(() => {
  486. // Get latest content
  487. return clientForProject.apis.Project.getLatestContent({
  488. project_id: testProjectId,
  489. })
  490. })
  491. .then(response => {
  492. // Check that no changes were stored
  493. const snapshot = Snapshot.fromRaw(response.obj)
  494. expect(snapshot.countFiles()).to.equal(1)
  495. expect(snapshot.getFile(testFilePathname).getHash()).to.equal(
  496. testFiles.NON_BMP_TXT_HASH
  497. )
  498. })
  499. })
  500. it('accepts text operation on files with null characters if stringLength is present', function () {
  501. const testProjectId = '1'
  502. const mainFilePathname = 'main.tex'
  503. const testTextOperation = TextOperation.fromJSON({
  504. textOperation: [3, 'a'],
  505. })
  506. let testSnapshot
  507. function importChanges() {
  508. const changes = [
  509. makeChange(Operation.editFile(mainFilePathname, testTextOperation)),
  510. ]
  511. return basicAuthClient.apis.ProjectImport.importChanges1({
  512. project_id: testProjectId,
  513. end_version: 0,
  514. changes: changes.map(changeToRaw),
  515. })
  516. }
  517. function getLatestContent() {
  518. return clientForProject.apis.Project.getLatestContent({
  519. project_id: testProjectId,
  520. })
  521. }
  522. return fetch(
  523. testServer.url(
  524. `/api/projects/${testProjectId}/blobs/${testFiles.NULL_CHARACTERS_TXT_HASH}`
  525. ),
  526. {
  527. method: 'PUT',
  528. body: fs.createReadStream(testFiles.path('null_characters.txt')),
  529. headers: {
  530. Authorization: testServer.basicAuthHeader,
  531. },
  532. }
  533. )
  534. .then(response => {
  535. expect(response.ok).to.be.true
  536. })
  537. .then(() => {
  538. // Import project
  539. testSnapshot = new Snapshot()
  540. testSnapshot.addFile(
  541. mainFilePathname,
  542. File.fromHash(testFiles.NULL_CHARACTERS_TXT_HASH)
  543. )
  544. return basicAuthClient.apis.ProjectImport.importSnapshot1({
  545. project_id: testProjectId,
  546. snapshot: testSnapshot.toRaw(),
  547. })
  548. })
  549. .then(importChanges)
  550. .then(() => {
  551. // Expect invalid changes to fail
  552. expect.fail()
  553. })
  554. .catch(expectResponse.unprocessableEntity)
  555. .then(getLatestContent)
  556. .then(response => {
  557. // Check that no changes were made
  558. const snapshot = Snapshot.fromRaw(response.obj)
  559. expect(snapshot.countFiles()).to.equal(1)
  560. expect(snapshot.getFile(mainFilePathname).getHash()).to.equal(
  561. testFiles.NULL_CHARACTERS_TXT_HASH
  562. )
  563. })
  564. .then(() => {
  565. // Set string length
  566. return knex('project_blobs').update(
  567. 'string_length',
  568. testFiles.NULL_CHARACTERS_TXT_BYTE_LENGTH
  569. )
  570. })
  571. .then(importChanges)
  572. .then(getLatestContent)
  573. .then(response => {
  574. const snapshot = Snapshot.fromRaw(response.obj)
  575. expect(snapshot.countFiles()).to.equal(1)
  576. expect(snapshot.getFile(mainFilePathname).getContent()).to.equal(
  577. '\x00\x00\x00a'
  578. )
  579. })
  580. })
  581. it('returns 404 when chunk is not found in bucket', function () {
  582. const testProjectId = '1'
  583. const fooChange = makeChange(Operation.removeFile('foo.tex'))
  584. return knex('chunks')
  585. .insert({
  586. doc_id: testProjectId,
  587. start_version: 0,
  588. end_version: 100,
  589. end_timestamp: null,
  590. })
  591. .then(() => {
  592. // Import changes
  593. return basicAuthClient.apis.ProjectImport.importChanges1({
  594. project_id: testProjectId,
  595. end_version: 100,
  596. changes: [fooChange.toRaw()],
  597. })
  598. })
  599. .then(() => {
  600. // Expect invalid changes to fail
  601. expect.fail()
  602. })
  603. .catch(expectResponse.notFound)
  604. })
  605. it('creates and returns changes with v2 author ids', function () {
  606. const testFilePathname = 'test.tex'
  607. const testTextOperation = TextOperation.fromJSON({ textOperation: ['a'] })
  608. const v2Authors = ['5a296963ad5e82432674c839', null]
  609. let testProjectId
  610. return testProjects
  611. .createEmptyProject()
  612. .then(projectId => {
  613. testProjectId = projectId
  614. expect(testProjectId).to.be.a('string')
  615. })
  616. .then(() => {
  617. return fetch(
  618. testServer.url(
  619. `/api/projects/${testProjectId}/blobs/${File.EMPTY_FILE_HASH}`
  620. ),
  621. {
  622. method: 'PUT',
  623. body: fs.createReadStream(testFiles.path('empty.tex')),
  624. headers: {
  625. Authorization: testServer.basicAuthHeader,
  626. },
  627. }
  628. )
  629. })
  630. .then(response => {
  631. expect(response.ok).to.be.true
  632. })
  633. .then(() => {
  634. // Import changes
  635. const testFile = File.fromHash(File.EMPTY_FILE_HASH)
  636. const changes = [
  637. makeChange(Operation.addFile(testFilePathname, testFile)),
  638. makeChange(Operation.editFile(testFilePathname, testTextOperation)),
  639. ]
  640. changes[1].setV2Authors(v2Authors)
  641. return basicAuthClient.apis.ProjectImport.importChanges1({
  642. project_id: testProjectId,
  643. end_version: 0,
  644. return_snapshot: 'hashed',
  645. changes: changes.map(changeToRaw),
  646. })
  647. })
  648. .then(response => {
  649. expect(response.status).to.equal(HTTPStatus.CREATED)
  650. const snapshot = Snapshot.fromRaw(response.obj)
  651. expect(snapshot.countFiles()).to.equal(1)
  652. expect(snapshot.getFile('test.tex').getHash()).to.equal(
  653. testFiles.STRING_A_HASH
  654. )
  655. })
  656. .then(() => {
  657. // Get project history
  658. return pseudoJwtBasicAuthClient.apis.Project.getLatestHistory({
  659. project_id: testProjectId,
  660. })
  661. })
  662. .then(response => {
  663. // it should not fail when the some of the authors do not exist anymore
  664. const chunkResponse = ChunkResponse.fromRaw(response.obj)
  665. const changes = chunkResponse.getChunk().getChanges()
  666. expect(changes.length).to.equal(2)
  667. const changeWithAuthor = changes[1]
  668. expect(changeWithAuthor.getV2Authors()).to.deep.equal(v2Authors)
  669. })
  670. })
  671. it('should reject invalid v2 author ids', function () {
  672. const testFilePathname = 'test.tex'
  673. const v2Authors = ['not-a-v2-id']
  674. let testProjectId
  675. return testProjects
  676. .createEmptyProject()
  677. .then(projectId => {
  678. testProjectId = projectId
  679. expect(testProjectId).to.be.a('string')
  680. })
  681. .then(() => {
  682. return fetch(
  683. testServer.url(
  684. `/api/projects/${testProjectId}/blobs/${File.EMPTY_FILE_HASH}`
  685. ),
  686. {
  687. method: 'PUT',
  688. body: fs.createReadStream(testFiles.path('empty.tex')),
  689. headers: {
  690. Authorization: testServer.basicAuthHeader,
  691. },
  692. }
  693. )
  694. })
  695. .then(response => {
  696. expect(response.ok).to.be.true
  697. })
  698. .then(() => {
  699. // Import changes
  700. const testFile = File.fromHash(File.EMPTY_FILE_HASH)
  701. const changes = [
  702. makeChange(Operation.addFile(testFilePathname, testFile)),
  703. ]
  704. changes[0].v2Authors = v2Authors
  705. return basicAuthClient.apis.ProjectImport.importChanges1({
  706. project_id: testProjectId,
  707. end_version: 0,
  708. changes: changes.map(changeToRaw),
  709. })
  710. })
  711. .then(() => {
  712. // Check that invalid changes fail
  713. expect.fail()
  714. })
  715. .catch(expectResponse.unprocessableEntity)
  716. })
  717. it('should reject changes with both v1 and v2 authors ids', function () {
  718. const testFilePathname = 'test.tex'
  719. const v1Authors = [456]
  720. const v2Authors = ['5a296963ad5e82432674c839', null]
  721. let testProjectId
  722. return testProjects
  723. .createEmptyProject()
  724. .then(projectId => {
  725. testProjectId = projectId
  726. expect(testProjectId).to.be.a('string')
  727. })
  728. .then(() => {
  729. return fetch(
  730. testServer.url(
  731. `/api/projects/${testProjectId}/blobs/${File.EMPTY_FILE_HASH}`
  732. ),
  733. {
  734. method: 'PUT',
  735. body: fs.createReadStream(testFiles.path('empty.tex')),
  736. headers: {
  737. Authorization: testServer.basicAuthHeader,
  738. },
  739. }
  740. )
  741. })
  742. .then(response => {
  743. expect(response.ok).to.be.true
  744. })
  745. .then(() => {
  746. // Import changes
  747. const testFile = File.fromHash(File.EMPTY_FILE_HASH)
  748. const changes = [
  749. makeChange(Operation.addFile(testFilePathname, testFile)),
  750. ]
  751. changes[0].authors = v1Authors
  752. changes[0].v2Authors = v2Authors
  753. return basicAuthClient.apis.ProjectImport.importChanges1({
  754. project_id: testProjectId,
  755. end_version: 0,
  756. changes: changes.map(changeToRaw),
  757. })
  758. })
  759. .then(() => {
  760. // Check that invalid changes fail
  761. expect.fail()
  762. })
  763. .catch(expectResponse.unprocessableEntity)
  764. })
  765. it("returns unprocessable if end_version isn't provided", function () {
  766. return testProjects
  767. .createEmptyProject()
  768. .then(projectId => {
  769. expect(projectId).to.be.a('string')
  770. return projectId
  771. })
  772. .then(projectId => {
  773. // Import changes
  774. return basicAuthClient.apis.ProjectImport.importChanges1({
  775. project_id: projectId,
  776. changes: [],
  777. })
  778. })
  779. .then(() => {
  780. // Check that invalid changes fail
  781. expect.fail()
  782. })
  783. .catch(error => {
  784. expect(error.message).to.equal(
  785. 'Required parameter end_version is not provided'
  786. )
  787. })
  788. })
  789. it('returns unprocessable if return_snapshot is invalid', function () {
  790. return testProjects
  791. .createEmptyProject()
  792. .then(projectId => {
  793. // Import changes
  794. return basicAuthClient.apis.ProjectImport.importChanges1({
  795. project_id: projectId,
  796. changes: [],
  797. end_version: 0,
  798. return_snapshot: 'not_a_valid_value',
  799. })
  800. })
  801. .then(() => {
  802. // Check that invalid changes fail
  803. expect.fail()
  804. })
  805. .catch(error => {
  806. expect(error.status).to.equal(HTTPStatus.UNPROCESSABLE_ENTITY)
  807. expect(error.response.body.message).to.equal(
  808. 'invalid enum value: return_snapshot'
  809. )
  810. })
  811. })
  812. })