project_updates.test.js 27 KB

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