RetryTests.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import nock from 'nock'
  2. import { expect } from 'chai'
  3. import { fetchStringWithResponse } from '@overleaf/fetch-utils'
  4. import assert from 'node:assert'
  5. import mongodb from 'mongodb-legacy'
  6. import * as ProjectHistoryClient from './helpers/ProjectHistoryClient.js'
  7. import * as ProjectHistoryApp from './helpers/ProjectHistoryApp.js'
  8. const { ObjectId } = mongodb
  9. const MockHistoryStore = () => nock('http://127.0.0.1:3100')
  10. const MockWeb = () => nock('http://127.0.0.1:3000')
  11. const MockCallback = () => nock('http://127.0.0.1')
  12. describe('Retrying failed projects', function () {
  13. const historyId = new ObjectId().toString()
  14. beforeEach(async function () {
  15. this.timestamp = new Date()
  16. await ProjectHistoryApp.ensureRunning()
  17. this.project_id = new ObjectId().toString()
  18. this.doc_id = new ObjectId().toString()
  19. this.file_id = new ObjectId().toString()
  20. MockHistoryStore().post('/api/projects').reply(200, {
  21. projectId: historyId,
  22. })
  23. MockWeb()
  24. .get(`/project/${this.project_id}/details`)
  25. .reply(200, {
  26. name: 'Test Project',
  27. overleaf: {
  28. history: {
  29. id: historyId,
  30. },
  31. },
  32. })
  33. MockHistoryStore()
  34. .get(`/api/projects/${historyId}/latest/history`)
  35. .reply(200, {
  36. chunk: {
  37. startVersion: 0,
  38. history: {
  39. changes: [],
  40. },
  41. },
  42. })
  43. await ProjectHistoryClient.initializeProject(historyId)
  44. })
  45. afterEach(function () {
  46. nock.cleanAll()
  47. })
  48. describe('retrying project history', function () {
  49. describe('when there is a soft failure', function () {
  50. beforeEach(async function () {
  51. this.flushCall = MockHistoryStore()
  52. .put(
  53. `/api/projects/${historyId}/blobs/0a207c060e61f3b88eaee0a8cd0696f46fb155eb`
  54. )
  55. .reply(201)
  56. .post(`/api/projects/${historyId}/legacy_changes?end_version=0`)
  57. .reply(200)
  58. const update = {
  59. pathname: '/main.tex',
  60. docLines: 'a\nb',
  61. doc: this.doc_id,
  62. meta: { user_id: this.user_id, ts: new Date() },
  63. }
  64. await ProjectHistoryClient.pushRawUpdate(this.project_id, update)
  65. await ProjectHistoryClient.setFailure({
  66. project_id: this.project_id,
  67. attempts: 1,
  68. error: 'soft-error',
  69. })
  70. })
  71. it('flushes the project history queue', async function () {
  72. const { response } = await fetchStringWithResponse(
  73. 'http://127.0.0.1:3054/retry/failures?failureType=soft&limit=1&timeout=10000',
  74. {
  75. method: 'POST',
  76. }
  77. )
  78. expect(response.status).to.equal(200)
  79. assert(
  80. this.flushCall.isDone(),
  81. 'made calls to history service to store updates'
  82. )
  83. })
  84. it('retries in the background when requested', async function () {
  85. this.callback = MockCallback()
  86. .matchHeader('Authorization', '123')
  87. .get('/ping')
  88. .reply(200)
  89. const { body, response } = await fetchStringWithResponse(
  90. 'http://127.0.0.1:3054/retry/failures?failureType=soft&limit=1&timeout=10000&callbackUrl=http%3A%2F%2F127.0.0.1%2Fping',
  91. {
  92. method: 'POST',
  93. headers: {
  94. 'X-CALLBACK-Authorization': '123',
  95. },
  96. }
  97. )
  98. expect(response.status).to.equal(200)
  99. expect(body).to.equal(
  100. '{"retryStatus":"running retryFailures in background"}'
  101. )
  102. assert(
  103. !this.flushCall.isDone(),
  104. 'did not make calls to history service to store updates in the foreground'
  105. )
  106. await new Promise(resolve => setTimeout(resolve, 100))
  107. assert(
  108. this.flushCall.isDone(),
  109. 'made calls to history service to store updates in the background'
  110. )
  111. assert(this.callback.isDone(), 'hit the callback url')
  112. })
  113. })
  114. describe('when there is a hard failure', function () {
  115. beforeEach(async function () {
  116. MockWeb()
  117. .get(`/project/${this.project_id}/details`)
  118. .reply(200, {
  119. name: 'Test Project',
  120. overleaf: {
  121. history: {
  122. id: historyId,
  123. },
  124. },
  125. })
  126. await ProjectHistoryClient.setFailure({
  127. project_id: this.project_id,
  128. attempts: 100,
  129. error: 'hard-error',
  130. })
  131. })
  132. it('calls web to resync the project', async function () {
  133. const resyncCall = MockWeb()
  134. .post(`/project/${this.project_id}/history/resync`)
  135. .reply(200)
  136. const { response } = await fetchStringWithResponse(
  137. 'http://127.0.0.1:3054/retry/failures?failureType=hard&limit=1&timeout=10000',
  138. {
  139. method: 'POST',
  140. }
  141. )
  142. expect(response.status).to.equal(200)
  143. assert(resyncCall.isDone(), 'made a call to web to resync project')
  144. })
  145. })
  146. })
  147. })