projects.test.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
  1. 'use strict'
  2. const { expect } = require('chai')
  3. const fs = require('node:fs')
  4. const { Readable } = require('node:stream')
  5. const HTTPStatus = require('http-status')
  6. const fetch = require('node-fetch')
  7. const sinon = require('sinon')
  8. const cleanup = require('../storage/support/cleanup')
  9. const fixtures = require('../storage/support/fixtures')
  10. const testFiles = require('../storage/support/test_files')
  11. const {
  12. zipStore,
  13. BlobStore,
  14. persistChanges,
  15. redisBuffer,
  16. blobHash,
  17. } = require('../../../../storage')
  18. const { expectHttpError } = require('./support/expect_response')
  19. const testServer = require('./support/test_server')
  20. const { createEmptyProject } = require('./support/test_projects')
  21. const {
  22. File,
  23. Snapshot,
  24. Change,
  25. AddFileOperation,
  26. EditFileOperation,
  27. TextOperation,
  28. } = require('overleaf-editor-core')
  29. const testProjects = require('./support/test_projects')
  30. const { ObjectId } = require('mongodb')
  31. describe('project controller', function () {
  32. beforeEach(cleanup.everything)
  33. beforeEach(fixtures.create)
  34. describe('initializeProject', function () {
  35. it('can initialize a new project', async function () {
  36. const projectId = await testProjects.createEmptyProject()
  37. expect(projectId).to.be.a('string')
  38. })
  39. })
  40. describe('createZip', function () {
  41. let importSnapshot
  42. let createZip
  43. before(function () {
  44. importSnapshot =
  45. testServer.basicAuthClient.apis.ProjectImport.importSnapshot1
  46. createZip = testServer.basicAuthClient.apis.Project.createZip
  47. })
  48. beforeEach(function () {
  49. // Don't start the work in the background in this test --- it is flaky.
  50. sinon.stub(zipStore, 'storeZip').resolves()
  51. })
  52. afterEach(function () {
  53. zipStore.storeZip.restore()
  54. })
  55. it('creates a URL to a zip file', async function () {
  56. // Create a test blob.
  57. const testProjectId = fixtures.docs.uninitializedProject.id
  58. const response = await fetch(
  59. testServer.url(
  60. `/api/projects/${testProjectId}/blobs/${testFiles.HELLO_TXT_HASH}`
  61. ),
  62. {
  63. method: 'PUT',
  64. body: fs.createReadStream(testFiles.path('hello.txt')),
  65. headers: {
  66. Authorization: testServer.basicAuthHeader,
  67. },
  68. }
  69. )
  70. expect(response.ok).to.be.true
  71. // Import a project with the test blob.
  72. const testFilePathname = 'hello.txt'
  73. const testSnapshot = new Snapshot()
  74. testSnapshot.addFile(
  75. testFilePathname,
  76. File.fromHash(testFiles.HELLO_TXT_HASH)
  77. )
  78. const importResponse = await importSnapshot({
  79. project_id: testProjectId,
  80. snapshot: testSnapshot.toRaw(),
  81. })
  82. expect(importResponse.obj.projectId).to.equal(testProjectId)
  83. const createZipResponse = await createZip({
  84. project_id: testProjectId,
  85. version: 0,
  86. })
  87. expect(createZipResponse.status).to.equal(HTTPStatus.OK)
  88. const zipInfo = createZipResponse.obj
  89. expect(zipInfo.zipUrl).to.match(
  90. /^http:\/\/gcs:9090\/download\/storage\/v1\/b\/overleaf-test-zips/
  91. )
  92. expect(zipStore.storeZip.calledOnce).to.be.true
  93. })
  94. })
  95. describe('blob stats', function () {
  96. let populatedPostgresProjectId,
  97. populatedMongoProjectId,
  98. emptyPostgresProjectId,
  99. emptyMongoProjectId
  100. async function populateProject(projectId) {
  101. const files = {
  102. [testFiles.GRAPH_PNG_HASH]: testFiles.path('graph.png'),
  103. [testFiles.HELLO_TXT_HASH]: testFiles.path('hello.txt'),
  104. }
  105. for (const [hash, path] of Object.entries(files)) {
  106. const response = await fetch(
  107. testServer.url(`/api/projects/${projectId}/blobs/${hash}`),
  108. {
  109. method: 'PUT',
  110. body: fs.createReadStream(path),
  111. headers: {
  112. Authorization: testServer.basicAuthHeader,
  113. },
  114. }
  115. )
  116. expect(response.status).to.equal(201)
  117. }
  118. }
  119. beforeEach(async function () {
  120. emptyPostgresProjectId = await testProjects.createEmptyProject()
  121. emptyMongoProjectId = await testProjects.createEmptyProject(
  122. new ObjectId().toString()
  123. )
  124. populatedPostgresProjectId = await testProjects.createEmptyProject()
  125. await populateProject(populatedPostgresProjectId)
  126. populatedMongoProjectId = await testProjects.createEmptyProject(
  127. new ObjectId().toString()
  128. )
  129. await populateProject(populatedMongoProjectId)
  130. })
  131. describe('getProjectBlobsStats', function () {
  132. it('handles empty postgres project', async function () {
  133. const { body } =
  134. await testServer.basicAuthClient.apis.Project.getProjectBlobsStats({
  135. body: { projectIds: [emptyPostgresProjectId] },
  136. })
  137. expect(body).to.deep.equal([
  138. {
  139. projectId: emptyPostgresProjectId,
  140. textBlobBytes: 0,
  141. binaryBlobBytes: 0,
  142. totalBytes: 0,
  143. nTextBlobs: 0,
  144. nBinaryBlobs: 0,
  145. },
  146. ])
  147. })
  148. it('handles populated postgres project', async function () {
  149. const { body } =
  150. await testServer.basicAuthClient.apis.Project.getProjectBlobsStats({
  151. body: { projectIds: [populatedPostgresProjectId] },
  152. })
  153. expect(body).to.deep.equal([
  154. {
  155. projectId: populatedPostgresProjectId,
  156. textBlobBytes: testFiles.HELLO_TXT_BYTE_LENGTH,
  157. binaryBlobBytes: testFiles.GRAPH_PNG_BYTE_LENGTH,
  158. totalBytes:
  159. testFiles.HELLO_TXT_BYTE_LENGTH + testFiles.GRAPH_PNG_BYTE_LENGTH,
  160. nTextBlobs: 1,
  161. nBinaryBlobs: 1,
  162. },
  163. ])
  164. })
  165. it('handles empty mongo project', async function () {
  166. const { body } =
  167. await testServer.basicAuthClient.apis.Project.getProjectBlobsStats({
  168. body: { projectIds: [emptyMongoProjectId] },
  169. })
  170. expect(body).to.deep.equal([
  171. {
  172. projectId: emptyMongoProjectId,
  173. textBlobBytes: 0,
  174. binaryBlobBytes: 0,
  175. totalBytes: 0,
  176. nTextBlobs: 0,
  177. nBinaryBlobs: 0,
  178. },
  179. ])
  180. })
  181. it('handles populated mongo project', async function () {
  182. const { body } =
  183. await testServer.basicAuthClient.apis.Project.getProjectBlobsStats({
  184. body: { projectIds: [populatedMongoProjectId] },
  185. })
  186. expect(body).to.deep.equal([
  187. {
  188. projectId: populatedMongoProjectId,
  189. textBlobBytes: testFiles.HELLO_TXT_BYTE_LENGTH,
  190. binaryBlobBytes: testFiles.GRAPH_PNG_BYTE_LENGTH,
  191. totalBytes:
  192. testFiles.HELLO_TXT_BYTE_LENGTH + testFiles.GRAPH_PNG_BYTE_LENGTH,
  193. nTextBlobs: 1,
  194. nBinaryBlobs: 1,
  195. },
  196. ])
  197. })
  198. it('handles batch of projects', async function () {
  199. const { body } =
  200. await testServer.basicAuthClient.apis.Project.getProjectBlobsStats({
  201. body: {
  202. projectIds: [
  203. populatedPostgresProjectId,
  204. populatedMongoProjectId,
  205. emptyPostgresProjectId,
  206. emptyMongoProjectId,
  207. ],
  208. },
  209. })
  210. expect(body).to.deep.equal([
  211. {
  212. projectId: populatedPostgresProjectId,
  213. textBlobBytes: testFiles.HELLO_TXT_BYTE_LENGTH,
  214. binaryBlobBytes: testFiles.GRAPH_PNG_BYTE_LENGTH,
  215. totalBytes:
  216. testFiles.HELLO_TXT_BYTE_LENGTH + testFiles.GRAPH_PNG_BYTE_LENGTH,
  217. nTextBlobs: 1,
  218. nBinaryBlobs: 1,
  219. },
  220. {
  221. projectId: populatedMongoProjectId,
  222. textBlobBytes: testFiles.HELLO_TXT_BYTE_LENGTH,
  223. binaryBlobBytes: testFiles.GRAPH_PNG_BYTE_LENGTH,
  224. totalBytes:
  225. testFiles.HELLO_TXT_BYTE_LENGTH + testFiles.GRAPH_PNG_BYTE_LENGTH,
  226. nTextBlobs: 1,
  227. nBinaryBlobs: 1,
  228. },
  229. {
  230. projectId: emptyPostgresProjectId,
  231. textBlobBytes: 0,
  232. binaryBlobBytes: 0,
  233. totalBytes: 0,
  234. nTextBlobs: 0,
  235. nBinaryBlobs: 0,
  236. },
  237. {
  238. projectId: emptyMongoProjectId,
  239. textBlobBytes: 0,
  240. binaryBlobBytes: 0,
  241. totalBytes: 0,
  242. nTextBlobs: 0,
  243. nBinaryBlobs: 0,
  244. },
  245. ])
  246. })
  247. })
  248. describe('getBlobStats', function () {
  249. it('handles empty list of hashes', async function () {
  250. const { body } =
  251. await testServer.basicAuthClient.apis.Project.getBlobStats({
  252. project_id: populatedPostgresProjectId,
  253. body: { blobHashes: [] },
  254. })
  255. expect(body).to.deep.equal({
  256. projectId: populatedPostgresProjectId,
  257. textBlobBytes: 0,
  258. binaryBlobBytes: 0,
  259. totalBytes: 0,
  260. nTextBlobs: 0,
  261. nBinaryBlobs: 0,
  262. })
  263. })
  264. it('handles a mix of text and binary blobs', async function () {
  265. const { body } =
  266. await testServer.basicAuthClient.apis.Project.getBlobStats({
  267. project_id: populatedPostgresProjectId,
  268. body: {
  269. blobHashes: [testFiles.HELLO_TXT_HASH, testFiles.GRAPH_PNG_HASH],
  270. },
  271. })
  272. expect(body).to.deep.equal({
  273. projectId: populatedPostgresProjectId,
  274. textBlobBytes: testFiles.HELLO_TXT_BYTE_LENGTH,
  275. binaryBlobBytes: testFiles.GRAPH_PNG_BYTE_LENGTH,
  276. totalBytes:
  277. testFiles.HELLO_TXT_BYTE_LENGTH + testFiles.GRAPH_PNG_BYTE_LENGTH,
  278. nTextBlobs: 1,
  279. nBinaryBlobs: 1,
  280. })
  281. })
  282. it('handles only text blobs', async function () {
  283. const { body } =
  284. await testServer.basicAuthClient.apis.Project.getBlobStats({
  285. project_id: populatedPostgresProjectId,
  286. body: {
  287. blobHashes: [testFiles.HELLO_TXT_HASH],
  288. },
  289. })
  290. expect(body).to.deep.equal({
  291. projectId: populatedPostgresProjectId,
  292. textBlobBytes: testFiles.HELLO_TXT_BYTE_LENGTH,
  293. binaryBlobBytes: 0,
  294. totalBytes: testFiles.HELLO_TXT_BYTE_LENGTH,
  295. nTextBlobs: 1,
  296. nBinaryBlobs: 0,
  297. })
  298. })
  299. it('handles only binary blobs', async function () {
  300. const { body } =
  301. await testServer.basicAuthClient.apis.Project.getBlobStats({
  302. project_id: populatedPostgresProjectId,
  303. body: {
  304. blobHashes: [testFiles.GRAPH_PNG_HASH],
  305. },
  306. })
  307. expect(body).to.deep.equal({
  308. projectId: populatedPostgresProjectId,
  309. textBlobBytes: 0,
  310. binaryBlobBytes: testFiles.GRAPH_PNG_BYTE_LENGTH,
  311. totalBytes: testFiles.GRAPH_PNG_BYTE_LENGTH,
  312. nTextBlobs: 0,
  313. nBinaryBlobs: 1,
  314. })
  315. })
  316. it('handles non-existent blobs', async function () {
  317. const { body } =
  318. await testServer.basicAuthClient.apis.Project.getBlobStats({
  319. project_id: populatedPostgresProjectId,
  320. body: {
  321. blobHashes: [testFiles.STRING_AB_HASH],
  322. },
  323. })
  324. expect(body).to.deep.equal({
  325. projectId: populatedPostgresProjectId,
  326. textBlobBytes: 0,
  327. binaryBlobBytes: 0,
  328. totalBytes: 0,
  329. nTextBlobs: 0,
  330. nBinaryBlobs: 0,
  331. })
  332. })
  333. it('throws an error for bad hashes', async function () {
  334. await expectHttpError(
  335. testServer.basicAuthClient.apis.Project.getBlobStats({
  336. project_id: populatedPostgresProjectId,
  337. body: {
  338. blobHashes: ['non-existent-hash'],
  339. },
  340. }),
  341. HTTPStatus.INTERNAL_SERVER_ERROR
  342. )
  343. })
  344. it('handles a request with a large number of blobs', async function () {
  345. const projectId = await testProjects.createEmptyProject()
  346. const blobHashes = []
  347. let expectedTextBytes = 0
  348. let expectedBinaryBytes = 0
  349. const nTextBlobs = 10
  350. const nBinaryBlobs = 10
  351. for (let i = 0; i < nTextBlobs; i++) {
  352. const content = `text blob ${i}`
  353. const hash = blobHash.fromString(content)
  354. blobHashes.push(hash)
  355. expectedTextBytes += content.length
  356. const res = await fetch(
  357. testServer.url(`/api/projects/${projectId}/blobs/${hash}`),
  358. {
  359. method: 'PUT',
  360. body: content,
  361. headers: { Authorization: testServer.basicAuthHeader },
  362. }
  363. )
  364. expect(res.status).to.equal(HTTPStatus.CREATED)
  365. }
  366. for (let i = 0; i < nBinaryBlobs; i++) {
  367. const content = Buffer.from([0, i, i + 1, i + 2])
  368. const hash = await blobHash.fromStream(
  369. content.length,
  370. Readable.from(content)
  371. )
  372. blobHashes.push(hash)
  373. expectedBinaryBytes += content.length
  374. const res = await fetch(
  375. testServer.url(`/api/projects/${projectId}/blobs/${hash}`),
  376. {
  377. method: 'PUT',
  378. body: content,
  379. headers: {
  380. Authorization: testServer.basicAuthHeader,
  381. 'Content-Type': 'application/octet-stream',
  382. },
  383. }
  384. )
  385. expect(res.status).to.equal(HTTPStatus.CREATED)
  386. }
  387. const { body } =
  388. await testServer.basicAuthClient.apis.Project.getBlobStats({
  389. project_id: projectId,
  390. body: { blobHashes },
  391. })
  392. expect(body).to.deep.equal({
  393. projectId,
  394. textBlobBytes: expectedTextBytes,
  395. binaryBlobBytes: expectedBinaryBytes,
  396. totalBytes: expectedTextBytes + expectedBinaryBytes,
  397. nTextBlobs,
  398. nBinaryBlobs,
  399. })
  400. })
  401. })
  402. })
  403. // eslint-disable-next-line mocha/no-skipped-tests
  404. describe.skip('getLatestContent', function () {
  405. // TODO: remove this endpoint entirely, see
  406. // https://github.com/overleaf/write_latex/pull/5120#discussion_r244291862
  407. })
  408. describe('project with changes', function () {
  409. let projectId
  410. beforeEach(async function () {
  411. // used to provide a limit which forces us to persist all of the changes.
  412. const farFuture = new Date()
  413. farFuture.setTime(farFuture.getTime() + 7 * 24 * 3600 * 1000)
  414. const limits = {
  415. minChangeTimestamp: farFuture,
  416. maxChangeTimestamp: farFuture,
  417. }
  418. const changes = [
  419. new Change(
  420. [new AddFileOperation('test.tex', File.fromString('ab'))],
  421. new Date(),
  422. []
  423. ),
  424. new Change(
  425. [new AddFileOperation('other.tex', File.fromString('hello'))],
  426. new Date(),
  427. []
  428. ),
  429. ]
  430. projectId = await createEmptyProject()
  431. await persistChanges(projectId, changes, limits, 0)
  432. })
  433. describe('getLatestHashedContent', function () {
  434. it('returns a snapshot', async function () {
  435. const response =
  436. await testServer.basicAuthClient.apis.Project.getLatestHashedContent({
  437. project_id: projectId,
  438. })
  439. expect(response.status).to.equal(HTTPStatus.OK)
  440. const snapshot = Snapshot.fromRaw(response.obj)
  441. expect(snapshot.countFiles()).to.equal(2)
  442. expect(snapshot.getFile('test.tex').getHash()).to.equal(
  443. testFiles.STRING_AB_HASH
  444. )
  445. })
  446. })
  447. describe('getChanges', function () {
  448. it('returns all changes when not given a limit', async function () {
  449. const response =
  450. await testServer.basicAuthClient.apis.Project.getChanges({
  451. project_id: projectId,
  452. })
  453. expect(response.status).to.equal(HTTPStatus.OK)
  454. const { changes, hasMore } = response.obj
  455. expect(changes.length).to.equal(2)
  456. const filenames = changes
  457. .flatMap(change => change.operations)
  458. .map(operation => operation.pathname)
  459. expect(filenames).to.deep.equal(['test.tex', 'other.tex'])
  460. expect(hasMore).to.be.false
  461. })
  462. it('returns only requested changes', async function () {
  463. const response =
  464. await testServer.basicAuthClient.apis.Project.getChanges({
  465. project_id: projectId,
  466. since: 1,
  467. })
  468. expect(response.status).to.equal(HTTPStatus.OK)
  469. const { changes, hasMore } = response.obj
  470. expect(changes.length).to.equal(1)
  471. const filenames = changes
  472. .flatMap(change => change.operations)
  473. .map(operation => operation.pathname)
  474. expect(filenames).to.deep.equal(['other.tex'])
  475. expect(hasMore).to.be.false
  476. })
  477. it('rejects negative versions', async function () {
  478. await expect(
  479. testServer.basicAuthClient.apis.Project.getChanges({
  480. project_id: projectId,
  481. since: -1,
  482. })
  483. ).to.be.rejectedWith('Bad Request')
  484. })
  485. it('rejects out of bounds versions', async function () {
  486. await expect(
  487. testServer.basicAuthClient.apis.Project.getChanges({
  488. project_id: projectId,
  489. since: 20,
  490. })
  491. ).to.be.rejectedWith('Bad Request')
  492. })
  493. })
  494. describe('project with many chunks', function () {
  495. let projectId, changes
  496. beforeEach(async function () {
  497. // used to provide a limit which forces us to persist all of the changes.
  498. const farFuture = new Date()
  499. farFuture.setTime(farFuture.getTime() + 7 * 24 * 3600 * 1000)
  500. const limits = {
  501. minChangeTimestamp: farFuture,
  502. maxChangeTimestamp: farFuture,
  503. maxChunkChanges: 5,
  504. }
  505. projectId = await createEmptyProject()
  506. const blobStore = new BlobStore(projectId)
  507. const blob = await blobStore.putString('')
  508. changes = [
  509. new Change(
  510. [new AddFileOperation('test.tex', File.createLazyFromBlobs(blob))],
  511. new Date(),
  512. []
  513. ),
  514. ]
  515. for (let i = 0; i < 20; i++) {
  516. const textOperation = new TextOperation()
  517. textOperation.retain(i)
  518. textOperation.insert('x')
  519. changes.push(
  520. new Change(
  521. [new EditFileOperation('test.tex', textOperation)],
  522. new Date(),
  523. []
  524. )
  525. )
  526. }
  527. await persistChanges(projectId, changes, limits, 0)
  528. })
  529. it('returns the first chunk when not given a limit', async function () {
  530. const response =
  531. await testServer.basicAuthClient.apis.Project.getChanges({
  532. project_id: projectId,
  533. })
  534. expect(response.status).to.equal(HTTPStatus.OK)
  535. expect(response.obj).to.deep.equal({
  536. changes: changes.slice(0, 5).map(c => c.toRaw()),
  537. hasMore: true,
  538. })
  539. })
  540. it('returns only requested changes', async function () {
  541. const response =
  542. await testServer.basicAuthClient.apis.Project.getChanges({
  543. project_id: projectId,
  544. since: 12,
  545. })
  546. expect(response.status).to.equal(HTTPStatus.OK)
  547. expect(response.obj).to.deep.equal({
  548. changes: changes.slice(12, 15).map(c => c.toRaw()),
  549. hasMore: true,
  550. })
  551. })
  552. it('returns changes in the latest chunk', async function () {
  553. const response =
  554. await testServer.basicAuthClient.apis.Project.getChanges({
  555. project_id: projectId,
  556. since: 20,
  557. })
  558. expect(response.status).to.equal(HTTPStatus.OK)
  559. expect(response.obj).to.deep.equal({
  560. changes: changes.slice(20).map(c => c.toRaw()),
  561. hasMore: false,
  562. })
  563. })
  564. })
  565. })
  566. describe('getLatestHistoryRaw', function () {
  567. it('should handles read', async function () {
  568. const projectId = fixtures.docs.initializedProject.id
  569. const response =
  570. await testServer.pseudoJwtBasicAuthClient.apis.Project.getLatestHistoryRaw(
  571. {
  572. project_id: projectId,
  573. readOnly: 'true',
  574. }
  575. )
  576. expect(response.body).to.deep.equal({
  577. startVersion: 0,
  578. endVersion: 1,
  579. endTimestamp: '2032-01-01T00:00:00.000Z',
  580. })
  581. })
  582. })
  583. describe('deleteProject', function () {
  584. it('deletes the project chunks', async function () {
  585. const projectId = fixtures.docs.initializedProject.id
  586. const historyResponse =
  587. await testServer.pseudoJwtBasicAuthClient.apis.Project.getLatestHistory(
  588. {
  589. project_id: projectId,
  590. }
  591. )
  592. expect(historyResponse.status).to.equal(HTTPStatus.OK)
  593. expect(historyResponse.body).to.have.property('chunk')
  594. const deleteResponse =
  595. await testServer.basicAuthClient.apis.Project.deleteProject({
  596. project_id: projectId,
  597. })
  598. expect(deleteResponse.status).to.equal(HTTPStatus.NO_CONTENT)
  599. await expectHttpError(
  600. testServer.pseudoJwtBasicAuthClient.apis.Project.getLatestHistory({
  601. project_id: projectId,
  602. }),
  603. HTTPStatus.NOT_FOUND
  604. )
  605. })
  606. it('deletes the project blobs', async function () {
  607. const projectId = fixtures.docs.initializedProject.id
  608. const token = testServer.createTokenForProject(projectId)
  609. const authHeaders = { Authorization: `Bearer ${token}` }
  610. const hash = testFiles.HELLO_TXT_HASH
  611. const fileContents = await fs.promises.readFile(
  612. testFiles.path('hello.txt')
  613. )
  614. const blobUrl = testServer.url(`/api/projects/${projectId}/blobs/${hash}`)
  615. const response1 = await fetch(blobUrl, {
  616. method: 'PUT',
  617. headers: authHeaders,
  618. body: fileContents,
  619. })
  620. expect(response1.ok).to.be.true
  621. const response2 = await fetch(blobUrl, { headers: authHeaders })
  622. const payload = await response2.text()
  623. expect(payload).to.equal(fileContents.toString())
  624. const deleteResponse =
  625. await testServer.basicAuthClient.apis.Project.deleteProject({
  626. project_id: projectId,
  627. })
  628. expect(deleteResponse.status).to.equal(HTTPStatus.NO_CONTENT)
  629. const response3 = await fetch(blobUrl, { headers: authHeaders })
  630. expect(response3.status).to.equal(HTTPStatus.NOT_FOUND)
  631. })
  632. it('deletes the project from the redis buffer', async function () {
  633. const projectId = await createEmptyProject()
  634. const blobStore = new BlobStore(projectId)
  635. const blob = await blobStore.putString('this is a test')
  636. const snapshot = new Snapshot()
  637. const change = new Change(
  638. [new AddFileOperation('test.tex', File.createLazyFromBlobs(blob))],
  639. new Date(),
  640. []
  641. )
  642. await redisBuffer.queueChanges(projectId, snapshot, 0, [change])
  643. const changesBefore = await redisBuffer.getNonPersistedChanges(
  644. projectId,
  645. 0
  646. )
  647. expect(changesBefore.length).to.equal(1)
  648. const deleteResponse =
  649. await testServer.basicAuthClient.apis.Project.deleteProject({
  650. project_id: projectId,
  651. })
  652. expect(deleteResponse.status).to.equal(HTTPStatus.NO_CONTENT)
  653. const changesAfter = await redisBuffer.getNonPersistedChanges(
  654. projectId,
  655. 0
  656. )
  657. expect(changesAfter.length).to.equal(0)
  658. const finalState = await redisBuffer.getState(projectId)
  659. expect(finalState).to.deep.equal({
  660. changes: [],
  661. expireTime: null,
  662. headSnapshot: null,
  663. headVersion: null,
  664. persistTime: null,
  665. persistedVersion: null,
  666. })
  667. })
  668. it('deletes an empty project from the redis buffer', async function () {
  669. const projectId = await createEmptyProject()
  670. const deleteResponse =
  671. await testServer.basicAuthClient.apis.Project.deleteProject({
  672. project_id: projectId,
  673. })
  674. expect(deleteResponse.status).to.equal(HTTPStatus.NO_CONTENT)
  675. const changesAfter = await redisBuffer.getNonPersistedChanges(
  676. projectId,
  677. 0
  678. )
  679. expect(changesAfter.length).to.equal(0)
  680. })
  681. })
  682. })