DocumentController.test.mjs 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. import { vi } from 'vitest'
  2. import sinon from 'sinon'
  3. import MockRequest from '../helpers/MockRequest.mjs'
  4. import MockResponse from '../helpers/MockResponse.mjs'
  5. import Errors from '../../../../app/src/Features/Errors/Errors.js'
  6. const MODULE_PATH =
  7. '../../../../app/src/Features/Documents/DocumentController.mjs'
  8. describe('DocumentController', function () {
  9. beforeEach(async function (ctx) {
  10. ctx.res = new MockResponse(vi)
  11. ctx.req = new MockRequest(vi)
  12. ctx.next = sinon.stub()
  13. ctx.doc = { _id: 'doc-id-123' }
  14. ctx.doc_lines = ['one', 'two', 'three']
  15. ctx.version = 42
  16. ctx.ranges = {
  17. comments: [
  18. {
  19. id: 'comment1',
  20. op: {
  21. c: 'foo',
  22. p: 123,
  23. t: 'comment1',
  24. },
  25. },
  26. {
  27. id: 'comment2',
  28. op: {
  29. c: 'bar',
  30. p: 456,
  31. t: 'comment2',
  32. },
  33. },
  34. ],
  35. }
  36. ctx.pathname = '/a/b/c/file.tex'
  37. ctx.lastUpdatedAt = new Date().getTime()
  38. ctx.lastUpdatedBy = 'fake-last-updater-id'
  39. ctx.rev = 5
  40. ctx.project = {
  41. _id: 'project-id-123',
  42. overleaf: {
  43. history: {
  44. id: 1234,
  45. display: true,
  46. },
  47. },
  48. }
  49. ctx.resolvedThreadIds = [
  50. 'comment2',
  51. 'comment4', // Comment in project but not in doc
  52. ]
  53. ctx.ProjectGetter = {
  54. promises: {
  55. getProject: sinon.stub().resolves(ctx.project),
  56. },
  57. }
  58. ctx.ProjectLocator = {
  59. promises: {
  60. findElement: sinon
  61. .stub()
  62. .resolves({ element: ctx.doc, path: { fileSystem: ctx.pathname } }),
  63. },
  64. }
  65. ctx.ProjectEntityHandler = {
  66. promises: {
  67. getDoc: sinon.stub().resolves({
  68. lines: ctx.doc_lines,
  69. rev: ctx.rev,
  70. version: ctx.version,
  71. ranges: ctx.ranges,
  72. }),
  73. },
  74. }
  75. ctx.ProjectEntityUpdateHandler = {
  76. promises: {
  77. updateDocLines: sinon.stub().resolves(),
  78. },
  79. }
  80. ctx.ChatApiHandler = {
  81. promises: {
  82. getResolvedThreadIds: sinon.stub().resolves(ctx.resolvedThreadIds),
  83. },
  84. }
  85. ctx.Modules = {
  86. promises: {
  87. hooks: {
  88. fire: sinon.stub().resolves(),
  89. },
  90. },
  91. }
  92. vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
  93. default: ctx.ProjectGetter,
  94. }))
  95. vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({
  96. default: ctx.ProjectLocator,
  97. }))
  98. vi.doMock(
  99. '../../../../app/src/Features/Project/ProjectEntityHandler',
  100. () => ({
  101. default: ctx.ProjectEntityHandler,
  102. })
  103. )
  104. vi.doMock(
  105. '../../../../app/src/Features/Project/ProjectEntityUpdateHandler',
  106. () => ({
  107. default: ctx.ProjectEntityUpdateHandler,
  108. })
  109. )
  110. vi.doMock('../../../../app/src/Features/Chat/ChatApiHandler', () => ({
  111. default: ctx.ChatApiHandler,
  112. }))
  113. vi.doMock('../../../../app/src/infrastructure/Modules.mjs', () => ({
  114. default: ctx.Modules,
  115. }))
  116. ctx.DocumentController = (await import(MODULE_PATH)).default
  117. })
  118. describe('getDocument', function () {
  119. beforeEach(function (ctx) {
  120. ctx.req.params = {
  121. Project_id: ctx.project._id,
  122. doc_id: ctx.doc._id,
  123. }
  124. })
  125. describe('when project exists with project history enabled', function () {
  126. beforeEach(async function (ctx) {
  127. await new Promise(resolve => {
  128. ctx.res.callback = err => {
  129. resolve(err)
  130. }
  131. ctx.DocumentController.getDocument(ctx.req, ctx.res, ctx.next)
  132. })
  133. })
  134. it('should return the history id and display setting to the client as JSON', function (ctx) {
  135. ctx.res.type.should.equal('application/json')
  136. JSON.parse(ctx.res.body).should.deep.equal({
  137. lines: ctx.doc_lines,
  138. version: ctx.version,
  139. ranges: ctx.ranges,
  140. pathname: ctx.pathname,
  141. projectHistoryId: ctx.project.overleaf.history.id,
  142. projectHistoryType: 'project-history',
  143. resolvedCommentIds: ['comment2'],
  144. historyRangesSupport: false,
  145. otMigrationStage: 0,
  146. })
  147. })
  148. })
  149. describe('when the project does not exist', function () {
  150. beforeEach(async function (ctx) {
  151. await new Promise(resolve => {
  152. ctx.ProjectGetter.promises.getProject.resolves(null)
  153. ctx.res.callback = err => {
  154. resolve(err)
  155. }
  156. ctx.DocumentController.getDocument(ctx.req, ctx.res, ctx.next)
  157. })
  158. })
  159. it('returns a 404', function (ctx) {
  160. ctx.res.statusCode.should.equal(404)
  161. })
  162. })
  163. })
  164. describe('setDocument', function () {
  165. beforeEach(function (ctx) {
  166. ctx.req.params = {
  167. Project_id: ctx.project._id,
  168. doc_id: ctx.doc._id,
  169. }
  170. })
  171. describe('when the document exists', function () {
  172. beforeEach(async function (ctx) {
  173. await new Promise(resolve => {
  174. ctx.req.body = {
  175. lines: ctx.doc_lines,
  176. version: ctx.version,
  177. ranges: ctx.ranges,
  178. lastUpdatedAt: ctx.lastUpdatedAt,
  179. lastUpdatedBy: ctx.lastUpdatedBy,
  180. }
  181. ctx.res.callback = err => {
  182. resolve(err)
  183. }
  184. ctx.DocumentController.setDocument(ctx.req, ctx.res, ctx.next)
  185. })
  186. })
  187. it('should update the document in Mongo', function (ctx) {
  188. sinon.assert.calledWith(
  189. ctx.ProjectEntityUpdateHandler.promises.updateDocLines,
  190. ctx.project._id,
  191. ctx.doc._id,
  192. ctx.doc_lines,
  193. ctx.version,
  194. ctx.ranges,
  195. ctx.lastUpdatedAt,
  196. ctx.lastUpdatedBy
  197. )
  198. })
  199. it('should return a successful response', function (ctx) {
  200. ctx.res.success.should.equal(true)
  201. })
  202. it('should call the docModified hook', function (ctx) {
  203. sinon.assert.calledWith(
  204. ctx.Modules.promises.hooks.fire,
  205. 'docModified',
  206. ctx.project._id,
  207. ctx.doc._id
  208. )
  209. })
  210. })
  211. describe("when the document doesn't exist", function () {
  212. beforeEach(async function (ctx) {
  213. await new Promise(resolve => {
  214. ctx.ProjectEntityUpdateHandler.promises.updateDocLines.rejects(
  215. new Errors.NotFoundError('document does not exist')
  216. )
  217. ctx.req.body = { lines: ctx.doc_lines }
  218. ctx.next.callsFake(() => {
  219. resolve()
  220. })
  221. ctx.DocumentController.setDocument(ctx.req, ctx.res, ctx.next)
  222. })
  223. })
  224. it('should call next with the NotFoundError', function (ctx) {
  225. ctx.next
  226. .calledWith(sinon.match.instanceOf(Errors.NotFoundError))
  227. .should.equal(true)
  228. })
  229. })
  230. })
  231. })