HistoryTests.mjs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import { expect } from 'chai'
  2. import _ from 'lodash'
  3. import { db, ObjectId } from '../../../../../app/src/infrastructure/mongodb.mjs'
  4. import User from '../../../../../test/acceptance/src/helpers/User.mjs'
  5. import MockV1HistoryApiClass from '../../../../../test/acceptance/src/mocks/MockV1HistoryApi.mjs'
  6. let MockV1HistoryApi
  7. before(function () {
  8. MockV1HistoryApi = MockV1HistoryApiClass.instance()
  9. })
  10. describe('History', function () {
  11. beforeEach(function (done) {
  12. this.owner = new User()
  13. this.owner.login(done)
  14. })
  15. describe('zip download of version', function () {
  16. it('should stream the zip file of a version', function (done) {
  17. this.owner.createProject('example-project', (error, projectId) => {
  18. this.project_id = projectId
  19. if (error) {
  20. return done(error)
  21. }
  22. this.v1_history_id = 42
  23. db.projects.updateOne(
  24. {
  25. _id: new ObjectId(this.project_id),
  26. },
  27. {
  28. $set: {
  29. 'overleaf.history.id': this.v1_history_id,
  30. },
  31. },
  32. error => {
  33. if (error) {
  34. return done(error)
  35. }
  36. this.owner.request(
  37. `/project/${this.project_id}/version/42/zip`,
  38. (error, response, body) => {
  39. if (error) {
  40. return done(error)
  41. }
  42. expect(response.statusCode).to.equal(200)
  43. expect(response.headers['content-type']).to.equal(
  44. 'application/zip'
  45. )
  46. expect(response.headers['content-disposition']).to.equal(
  47. 'attachment; filename="example-project (Version 42).zip"'
  48. )
  49. expect(body).to.equal(
  50. `Mock zip for ${this.v1_history_id} at version 42`
  51. )
  52. done()
  53. }
  54. )
  55. }
  56. )
  57. })
  58. })
  59. describe('request abort', function () {
  60. // Optional manual verification: add unique logging statements into
  61. // HistoryController._pipeHistoryZipToResponse
  62. // in each of the `req.destroyed` branches and confirm that each branch
  63. // was covered.
  64. beforeEach(function setupNewProject(done) {
  65. this.owner.createProject('example-project', (error, projectId) => {
  66. this.project_id = projectId
  67. if (error) {
  68. return done(error)
  69. }
  70. this.v1_history_id = 42
  71. db.projects.updateOne(
  72. { _id: new ObjectId(this.project_id) },
  73. {
  74. $set: {
  75. 'overleaf.history.id': this.v1_history_id,
  76. },
  77. },
  78. done
  79. )
  80. })
  81. })
  82. it('should abort the upstream request', function (done) {
  83. const request = this.owner.request(
  84. `/project/${this.project_id}/version/100/zip`
  85. )
  86. request.on('error', err => {
  87. if (err.code !== 'ECONNRESET') {
  88. done(err)
  89. }
  90. })
  91. request.on('response', response => {
  92. expect(response.statusCode).to.equal(200)
  93. let receivedChunks = 0
  94. response.on('data', () => {
  95. receivedChunks++
  96. })
  97. response.resume()
  98. setTimeout(() => {
  99. request.abort()
  100. const receivedSoFar = receivedChunks
  101. const sentSoFar = MockV1HistoryApi.sentChunks
  102. // Ihe next assertions should verify that chunks are emitted
  103. // and received -- the exact number is not important.
  104. // In theory we are now emitting the 3rd chunk,
  105. // so this should be exactly 3, to not make this
  106. // test flaky, we allow +- 2 chunks.
  107. expect(sentSoFar).to.be.within(1, 4)
  108. expect(receivedSoFar).to.be.within(1, 4)
  109. setTimeout(() => {
  110. // The fake-s3 service should have stopped emitting chunks.
  111. // If not, that would be +5 in an ideal world (1 every 100ms).
  112. // On the happy-path (it stopped) it emitted +1 which was
  113. // in-flight and another +1 before it received the abort.
  114. expect(MockV1HistoryApi.sentChunks).to.be.below(sentSoFar + 5)
  115. expect(MockV1HistoryApi.sentChunks).to.be.within(
  116. sentSoFar,
  117. sentSoFar + 2
  118. )
  119. done()
  120. }, 500)
  121. }, 200)
  122. })
  123. })
  124. it('should skip the v1-history request', function (done) {
  125. const request = this.owner.request(
  126. `/project/${this.project_id}/version/100/zip`
  127. )
  128. setTimeout(() => {
  129. // This is a race-condition to abort the request after the
  130. // processing of all the the express middleware completed.
  131. // In case we abort before they complete, we do not hit our
  132. // abort logic, but express internal logic, which is OK.
  133. request.abort()
  134. }, 2)
  135. request.on('error', done)
  136. setTimeout(() => {
  137. expect(MockV1HistoryApi.requestedZipPacks).to.equal(0)
  138. done()
  139. }, 500)
  140. })
  141. it('should skip the async-polling', function (done) {
  142. const request = this.owner.request(
  143. `/project/${this.project_id}/version/100/zip`
  144. )
  145. MockV1HistoryApi.events.on('v1-history-pack-zip', () => {
  146. request.abort()
  147. })
  148. request.on('error', done)
  149. setTimeout(() => {
  150. expect(MockV1HistoryApi.fakeZipCall).to.equal(0)
  151. done()
  152. }, 3000) // initial polling delay is 2s
  153. })
  154. it('should skip the upstream request', function (done) {
  155. const request = this.owner.request(
  156. `/project/${this.project_id}/version/100/zip`
  157. )
  158. MockV1HistoryApi.events.on('v1-history-pack-zip', () => {
  159. setTimeout(() => {
  160. request.abort()
  161. }, 1000)
  162. })
  163. request.on('error', done)
  164. setTimeout(() => {
  165. expect(MockV1HistoryApi.fakeZipCall).to.equal(0)
  166. done()
  167. }, 3000) // initial polling delay is 2s
  168. })
  169. })
  170. it('should return 402 for non-v2-history project', function (done) {
  171. this.owner.createProject('non-v2-project', (error, projectId) => {
  172. this.project_id = projectId
  173. if (error) {
  174. return done(error)
  175. }
  176. db.projects.updateOne(
  177. {
  178. _id: new ObjectId(this.project_id),
  179. },
  180. {
  181. $unset: {
  182. 'overleaf.history.id': true,
  183. },
  184. },
  185. error => {
  186. if (error) {
  187. return done(error)
  188. }
  189. this.owner.request(
  190. `/project/${this.project_id}/version/42/zip`,
  191. (error, response, body) => {
  192. if (error) {
  193. return done(error)
  194. }
  195. expect(response.statusCode).to.equal(402)
  196. done()
  197. }
  198. )
  199. }
  200. )
  201. })
  202. })
  203. })
  204. describe('zip download, with upstream 404', function () {
  205. beforeEach(function () {
  206. _.remove(
  207. MockV1HistoryApi.app._router.stack,
  208. appRoute =>
  209. appRoute.route?.path ===
  210. '/api/projects/:project_id/version/:version/zip'
  211. )
  212. MockV1HistoryApi.app.post(
  213. '/api/projects/:project_id/version/:version/zip',
  214. (req, res, next) => {
  215. res.sendStatus(404)
  216. }
  217. )
  218. })
  219. afterEach(function () {
  220. MockV1HistoryApi = MockV1HistoryApiClass.instance()
  221. MockV1HistoryApi.reset()
  222. MockV1HistoryApi.applyRoutes()
  223. })
  224. it('should produce 404 when post request produces 404', function (done) {
  225. this.owner.createProject('example-project', (error, projectId) => {
  226. if (error) {
  227. return done(error)
  228. }
  229. this.project_id = projectId
  230. this.v1_history_id = 42
  231. db.projects.updateOne(
  232. {
  233. _id: new ObjectId(this.project_id),
  234. },
  235. {
  236. $set: {
  237. 'overleaf.history.id': this.v1_history_id,
  238. },
  239. },
  240. error => {
  241. if (error) {
  242. return done(error)
  243. }
  244. this.owner.request(
  245. `/project/${this.project_id}/version/42/zip`,
  246. (error, response, body) => {
  247. if (error) {
  248. return done(error)
  249. }
  250. expect(response.statusCode).to.equal(404)
  251. done()
  252. }
  253. )
  254. }
  255. )
  256. })
  257. })
  258. })
  259. describe('zip download, with no zipUrl from upstream', function () {
  260. beforeEach(function () {
  261. _.remove(
  262. MockV1HistoryApi.app._router.stack,
  263. appRoute =>
  264. appRoute.route?.path ===
  265. '/api/projects/:project_id/version/:version/zip'
  266. )
  267. MockV1HistoryApi.app.get(
  268. '/api/projects/:project_id/version/:version/zip',
  269. (req, res, next) => {
  270. res.sendStatus(500)
  271. }
  272. )
  273. MockV1HistoryApi.app.post(
  274. '/api/projects/:project_id/version/:version/zip',
  275. (req, res, next) => {
  276. res.json({ message: 'lol' })
  277. }
  278. )
  279. })
  280. afterEach(function () {
  281. MockV1HistoryApi = MockV1HistoryApiClass.instance()
  282. MockV1HistoryApi.reset()
  283. MockV1HistoryApi.applyRoutes()
  284. })
  285. it('should produce 500', function (done) {
  286. this.owner.createProject('example-project', (error, projectId) => {
  287. if (error) {
  288. return done(error)
  289. }
  290. this.project_id = projectId
  291. this.v1_history_id = 42
  292. db.projects.updateOne(
  293. {
  294. _id: new ObjectId(this.project_id),
  295. },
  296. {
  297. $set: {
  298. 'overleaf.history.id': this.v1_history_id,
  299. },
  300. },
  301. error => {
  302. if (error) {
  303. return done(error)
  304. }
  305. this.owner.request(
  306. `/project/${this.project_id}/version/42/zip`,
  307. (error, response, body) => {
  308. if (error) {
  309. return done(error)
  310. }
  311. expect(response.statusCode).to.equal(500)
  312. done()
  313. }
  314. )
  315. }
  316. )
  317. })
  318. })
  319. })
  320. })