HistoryTests.mjs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import fs from 'node:fs'
  2. import Path from 'node:path'
  3. import { expect } from 'chai'
  4. import UserHelper from './helpers/User.mjs'
  5. import MockV1HistoryApiClass from './mocks/MockV1HistoryApi.mjs'
  6. import ProjectGetter from '../../../app/src/Features/Project/ProjectGetter.js'
  7. import { fileURLToPath } from 'node:url'
  8. import sinon from 'sinon'
  9. import logger from '@overleaf/logger'
  10. import Metrics from './helpers/metrics.mjs'
  11. const User = UserHelper.promises
  12. let MockV1HistoryApi
  13. before(function () {
  14. MockV1HistoryApi = MockV1HistoryApiClass.instance()
  15. })
  16. const __dirname = fileURLToPath(new URL('.', import.meta.url))
  17. const fileContent = fs.readFileSync(
  18. Path.join(__dirname, '../files/2pixel.png'),
  19. 'utf-8'
  20. )
  21. describe('HistoryTests', function () {
  22. let user, projectId, fileId, fileHash, fileURL, blobURL
  23. let historySource
  24. async function getSourceMetric(source) {
  25. return await Metrics.promises.getMetric(
  26. line => line.includes('request_blob') && line.includes(source)
  27. )
  28. }
  29. beforeEach('create project', async function () {
  30. user = new User()
  31. await user.login()
  32. projectId = await user.createProject('project1')
  33. const project = await ProjectGetter.promises.getProject(projectId)
  34. ;({ entity_id: fileId, hash: fileHash } =
  35. await user.uploadFileInProjectFull(
  36. projectId,
  37. project.rootFolder[0]._id.toString(),
  38. '2pixel.png',
  39. '2pixel.png',
  40. 'image/png'
  41. ))
  42. fileURL = `/project/${projectId}/file/${fileId}`
  43. blobURL = `/project/${projectId}/blob/${fileHash}`
  44. historySource = await getSourceMetric('history-v1')
  45. })
  46. async function expectHistoryV1Hit() {
  47. expect(await getSourceMetric('history-v1')).to.equal(historySource + 1)
  48. }
  49. async function expectNoIncrement() {
  50. expect(await getSourceMetric('history-v1')).to.equal(historySource)
  51. }
  52. describe('/project/:projectId/download/zip', function () {
  53. let spy, downloadZIPURL
  54. beforeEach(async function () {
  55. spy = sinon.spy(logger, 'error')
  56. downloadZIPURL = `/project/${projectId}/download/zip`
  57. })
  58. afterEach(function () {
  59. spy.restore()
  60. })
  61. it('should work from history-v1', async function () {
  62. const { response, body } = await user.doRequest('GET', downloadZIPURL)
  63. expect(response.statusCode).to.equal(200)
  64. expect(body).to.include('2pixel.png')
  65. await expectHistoryV1Hit()
  66. })
  67. it('should not include when missing', async function () {
  68. MockV1HistoryApi.reset()
  69. const { response, body } = await user.doRequest('GET', downloadZIPURL)
  70. expect(response.statusCode).to.equal(200)
  71. expect(
  72. spy.args.find(([, msg]) => msg === 'error adding files to zip stream')
  73. ).to.exist
  74. expect(body).to.not.include('2pixel.png')
  75. await expectNoIncrement()
  76. })
  77. })
  78. describe('/project/:projectId/blob/:hash', function () {
  79. describe('HEAD', function () {
  80. it('should fetch the file size from history-v1', async function () {
  81. const { response } = await user.doRequest('HEAD', blobURL)
  82. expect(response.statusCode).to.equal(200)
  83. expect(response.headers['content-length']).to.equal('3694')
  84. await expectHistoryV1Hit()
  85. })
  86. it('should return 404 without fallback', async function () {
  87. MockV1HistoryApi.reset()
  88. const { response } = await user.doRequest('HEAD', blobURL)
  89. expect(response.statusCode).to.equal(404)
  90. await expectNoIncrement()
  91. })
  92. })
  93. describe('GET', function () {
  94. it('should fetch the file from history-v1', async function () {
  95. const { response, body } = await user.doRequest('GET', blobURL)
  96. expect(response.statusCode).to.equal(200)
  97. expect(body).to.equal(fileContent)
  98. await expectHistoryV1Hit()
  99. })
  100. it('should set cache headers', async function () {
  101. const { response } = await user.doRequest('GET', blobURL)
  102. expect(response.headers['cache-control']).to.equal(
  103. 'private, max-age=86400, stale-while-revalidate=31536000'
  104. )
  105. expect(response.headers.etag).to.equal(fileHash)
  106. })
  107. it('should return a 304 when revalidating', async function () {
  108. const { response, body } = await user.doRequest('GET', {
  109. url: blobURL,
  110. headers: { 'If-None-Match': fileHash },
  111. })
  112. expect(response.statusCode).to.equal(304)
  113. expect(response.headers.etag).to.equal(fileHash)
  114. expect(body).to.equal('')
  115. })
  116. it('should return 404 without fallback', async function () {
  117. MockV1HistoryApi.reset()
  118. const { response } = await user.doRequest('GET', blobURL)
  119. expect(response.statusCode).to.equal(404)
  120. await expectNoIncrement()
  121. })
  122. it('should not set cache headers on 404', async function () {
  123. MockV1HistoryApi.reset()
  124. const { response } = await user.doRequest('GET', blobURL)
  125. expect(response.statusCode).to.equal(404)
  126. expect(response.headers).not.to.have.property('cache-control')
  127. expect(response.headers).not.to.have.property('etag')
  128. })
  129. })
  130. })
  131. // Legacy endpoint that is powered by history-v1 in SaaS
  132. describe('/project/:projectId/file/:fileId', function () {
  133. describe('HEAD', function () {
  134. it('should fetch the file size from history-v1', async function () {
  135. const { response } = await user.doRequest('HEAD', fileURL)
  136. expect(response.statusCode).to.equal(200)
  137. expect(response.headers['content-length']).to.equal('3694')
  138. await expectHistoryV1Hit()
  139. })
  140. it('should return 404 with both files missing', async function () {
  141. MockV1HistoryApi.reset()
  142. const { response } = await user.doRequest('HEAD', blobURL)
  143. expect(response.statusCode).to.equal(404)
  144. })
  145. })
  146. describe('GET', function () {
  147. it('should fetch the file from history-v1', async function () {
  148. const { response, body } = await user.doRequest('GET', fileURL)
  149. expect(response.statusCode).to.equal(200)
  150. expect(body).to.equal(fileContent)
  151. await expectHistoryV1Hit()
  152. })
  153. it('should set cache headers', async function () {
  154. const { response } = await user.doRequest('GET', fileURL)
  155. expect(response.headers['cache-control']).to.equal(
  156. 'private, max-age=3600'
  157. )
  158. })
  159. it('should not set cache headers on 404', async function () {
  160. MockV1HistoryApi.reset()
  161. // The legacy filestore downloads are not properly handling 404s, so delete the file from the file-tree to trigger the 404. All the filestore code will be removed soon.
  162. await user.doRequest('DELETE', fileURL)
  163. const { response } = await user.doRequest('GET', fileURL)
  164. expect(response.statusCode).to.equal(404)
  165. expect(response.headers).not.to.have.property('cache-control')
  166. expect(response.headers).not.to.have.property('etag')
  167. })
  168. it('should return 404 when missing', async function () {
  169. MockV1HistoryApi.reset()
  170. const { response } = await user.doRequest('GET', fileURL)
  171. expect(response.statusCode).to.equal(404)
  172. })
  173. })
  174. })
  175. })