ReferencesHandler.test.mjs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. import { expect, vi } from 'vitest'
  2. import sinon from 'sinon'
  3. import Errors from '../../../../app/src/Features/Errors/Errors.js'
  4. const modulePath =
  5. '../../../../app/src/Features/References/ReferencesHandler.mjs'
  6. vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
  7. vi.importActual('../../../../app/src/Features/Errors/Errors.js')
  8. )
  9. describe('ReferencesHandler', function () {
  10. beforeEach(async function (ctx) {
  11. ctx.projectId = '222'
  12. ctx.historyId = 42
  13. ctx.fakeProject = {
  14. _id: ctx.projectId,
  15. owner_ref: (ctx.fakeOwner = {
  16. _id: 'some_owner',
  17. features: {
  18. references: false,
  19. },
  20. }),
  21. rootFolder: [
  22. {
  23. docs: [
  24. { name: 'one.bib', _id: 'aaa' },
  25. { name: 'two.txt', _id: 'bbb' },
  26. ],
  27. folders: [
  28. {
  29. docs: [{ name: 'three.bib', _id: 'ccc' }],
  30. fileRefs: [
  31. { name: 'four.bib', _id: 'fff', hash: 'abc' },
  32. { name: 'five.bib', _id: 'ggg', hash: 'def' },
  33. ],
  34. folders: [],
  35. },
  36. ],
  37. },
  38. ],
  39. overleaf: { history: { id: ctx.historyId } },
  40. }
  41. ctx.docIds = ['aaa', 'ccc']
  42. vi.doMock('@overleaf/settings', () => ({
  43. default: (ctx.settings = {
  44. apis: {
  45. references: { url: 'http://some.url/references' },
  46. docstore: { url: 'http://some.url/docstore' },
  47. filestore: { url: 'http://some.url/filestore' },
  48. project_history: { url: 'http://project-history.local' },
  49. },
  50. }),
  51. }))
  52. vi.doMock('request', () => ({
  53. default: (ctx.request = {
  54. get: sinon.stub(),
  55. post: sinon.stub(),
  56. }),
  57. }))
  58. vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
  59. default: (ctx.ProjectGetter = {
  60. getProject: sinon.stub().callsArgWith(2, null, ctx.fakeProject),
  61. }),
  62. }))
  63. vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
  64. default: (ctx.UserGetter = {
  65. getUser: sinon.stub(),
  66. }),
  67. }))
  68. vi.doMock(
  69. '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler',
  70. () => ({
  71. default: (ctx.DocumentUpdaterHandler = {
  72. flushDocToMongo: sinon.stub().callsArgWith(2, null),
  73. }),
  74. })
  75. )
  76. vi.doMock('../../../../app/src/infrastructure/Features', () => ({
  77. default: (ctx.Features = {
  78. hasFeature: sinon.stub().returns(true),
  79. }),
  80. }))
  81. ctx.handler = (await import(modulePath)).default
  82. ctx.fakeResponseData = {
  83. projectId: ctx.projectId,
  84. keys: ['k1', 'k2'],
  85. }
  86. })
  87. describe('indexAll', function () {
  88. beforeEach(function (ctx) {
  89. sinon.stub(ctx.handler, '_findBibDocIds').returns(['aaa', 'ccc'])
  90. sinon.stub(ctx.handler, '_findBibFileRefs').returns([
  91. { _id: 'fff', hash: 'abc' },
  92. { _id: 'ggg', hash: 'def' },
  93. ])
  94. sinon.stub(ctx.handler, '_isFullIndex').callsArgWith(1, null, true)
  95. ctx.request.post.callsArgWith(
  96. 1,
  97. null,
  98. { statusCode: 200 },
  99. ctx.fakeResponseData
  100. )
  101. return (ctx.call = callback => {
  102. return ctx.handler.indexAll(ctx.projectId, callback)
  103. })
  104. })
  105. it('should call _findBibDocIds', async function (ctx) {
  106. await new Promise(resolve => {
  107. return ctx.call((err, data) => {
  108. expect(err).to.be.null
  109. ctx.handler._findBibDocIds.callCount.should.equal(1)
  110. ctx.handler._findBibDocIds
  111. .calledWith(ctx.fakeProject)
  112. .should.equal(true)
  113. return resolve()
  114. })
  115. })
  116. })
  117. it('should call _findBibFileRefs', async function (ctx) {
  118. await new Promise(resolve => {
  119. return ctx.call((err, data) => {
  120. expect(err).to.be.null
  121. ctx.handler._findBibDocIds.callCount.should.equal(1)
  122. ctx.handler._findBibDocIds
  123. .calledWith(ctx.fakeProject)
  124. .should.equal(true)
  125. return resolve()
  126. })
  127. })
  128. })
  129. it('should call DocumentUpdaterHandler.flushDocToMongo', async function (ctx) {
  130. await new Promise(resolve => {
  131. return ctx.call((err, data) => {
  132. expect(err).to.be.null
  133. ctx.DocumentUpdaterHandler.flushDocToMongo.callCount.should.equal(2)
  134. return resolve()
  135. })
  136. })
  137. })
  138. it('should make a request to references service', async function (ctx) {
  139. await new Promise(resolve => {
  140. return ctx.call((err, data) => {
  141. expect(err).to.be.null
  142. ctx.request.post.callCount.should.equal(1)
  143. const arg = ctx.request.post.firstCall.args[0]
  144. expect(arg.json).to.have.all.keys(
  145. 'docUrls',
  146. 'sourceURLs',
  147. 'fullIndex'
  148. )
  149. expect(arg.json.docUrls.length).to.equal(4)
  150. expect(arg.json.docUrls).to.deep.equal([
  151. `${ctx.settings.apis.docstore.url}/project/${ctx.projectId}/doc/aaa/raw`,
  152. `${ctx.settings.apis.docstore.url}/project/${ctx.projectId}/doc/ccc/raw`,
  153. `${ctx.settings.apis.project_history.url}/project/${ctx.historyId}/blob/abc`,
  154. `${ctx.settings.apis.project_history.url}/project/${ctx.historyId}/blob/def`,
  155. ])
  156. expect(arg.json.sourceURLs.length).to.equal(4)
  157. expect(arg.json.sourceURLs).to.deep.equal([
  158. {
  159. url: `${ctx.settings.apis.docstore.url}/project/${ctx.projectId}/doc/aaa/raw`,
  160. },
  161. {
  162. url: `${ctx.settings.apis.docstore.url}/project/${ctx.projectId}/doc/ccc/raw`,
  163. },
  164. {
  165. url: `${ctx.settings.apis.project_history.url}/project/${ctx.historyId}/blob/abc`,
  166. },
  167. {
  168. url: `${ctx.settings.apis.project_history.url}/project/${ctx.historyId}/blob/def`,
  169. },
  170. ])
  171. expect(arg.json.fullIndex).to.equal(true)
  172. return resolve()
  173. })
  174. })
  175. })
  176. it('should not produce an error', async function (ctx) {
  177. await new Promise(resolve => {
  178. return ctx.call((err, data) => {
  179. expect(err).to.equal(null)
  180. return resolve()
  181. })
  182. })
  183. })
  184. it('should return data', async function (ctx) {
  185. await new Promise(resolve => {
  186. return ctx.call((err, data) => {
  187. expect(err).to.be.null
  188. expect(data).to.not.equal(null)
  189. expect(data).to.not.equal(undefined)
  190. expect(data).to.equal(ctx.fakeResponseData)
  191. return resolve()
  192. })
  193. })
  194. })
  195. describe('when ProjectGetter.getProject produces an error', function () {
  196. beforeEach(function (ctx) {
  197. ctx.ProjectGetter.getProject.callsArgWith(2, new Error('woops'))
  198. })
  199. it('should produce an error', async function (ctx) {
  200. await new Promise(resolve => {
  201. ctx.call((err, data) => {
  202. expect(err).to.not.equal(null)
  203. expect(err).to.be.instanceof(Error)
  204. expect(data).to.equal(undefined)
  205. resolve()
  206. })
  207. })
  208. })
  209. it('should not send request', async function (ctx) {
  210. await new Promise(resolve => {
  211. ctx.call(() => {
  212. ctx.request.post.callCount.should.equal(0)
  213. resolve()
  214. })
  215. })
  216. })
  217. })
  218. describe('when ProjectGetter.getProject returns null', function () {
  219. beforeEach(function (ctx) {
  220. ctx.ProjectGetter.getProject.callsArgWith(2, null)
  221. })
  222. it('should produce an error', async function (ctx) {
  223. await new Promise(resolve => {
  224. ctx.call((err, data) => {
  225. expect(err).to.not.equal(null)
  226. expect(err).to.be.instanceof(Errors.NotFoundError)
  227. expect(data).to.equal(undefined)
  228. resolve()
  229. })
  230. })
  231. })
  232. it('should not send request', async function (ctx) {
  233. await new Promise(resolve => {
  234. ctx.call(() => {
  235. ctx.request.post.callCount.should.equal(0)
  236. resolve()
  237. })
  238. })
  239. })
  240. })
  241. describe('when _isFullIndex produces an error', function () {
  242. beforeEach(function (ctx) {
  243. ctx.ProjectGetter.getProject.callsArgWith(2, null, ctx.fakeProject)
  244. ctx.handler._isFullIndex.callsArgWith(1, new Error('woops'))
  245. })
  246. it('should produce an error', async function (ctx) {
  247. await new Promise(resolve => {
  248. ctx.call((err, data) => {
  249. expect(err).to.not.equal(null)
  250. expect(err).to.be.instanceof(Error)
  251. expect(data).to.equal(undefined)
  252. resolve()
  253. })
  254. })
  255. })
  256. it('should not send request', async function (ctx) {
  257. await new Promise(resolve => {
  258. ctx.call(() => {
  259. ctx.request.post.callCount.should.equal(0)
  260. resolve()
  261. })
  262. })
  263. })
  264. })
  265. describe('when flushDocToMongo produces an error', function () {
  266. beforeEach(function (ctx) {
  267. ctx.ProjectGetter.getProject.callsArgWith(2, null, ctx.fakeProject)
  268. ctx.handler._isFullIndex.callsArgWith(1, false)
  269. ctx.DocumentUpdaterHandler.flushDocToMongo.callsArgWith(
  270. 2,
  271. new Error('woops')
  272. )
  273. })
  274. it('should produce an error', async function (ctx) {
  275. await new Promise(resolve => {
  276. ctx.call((err, data) => {
  277. expect(err).to.not.equal(null)
  278. expect(err).to.be.instanceof(Error)
  279. expect(data).to.equal(undefined)
  280. resolve()
  281. })
  282. })
  283. })
  284. it('should not send request', async function (ctx) {
  285. await new Promise(resolve => {
  286. ctx.call(() => {
  287. ctx.request.post.callCount.should.equal(0)
  288. resolve()
  289. })
  290. })
  291. })
  292. })
  293. })
  294. describe('_findBibDocIds', function () {
  295. beforeEach(function (ctx) {
  296. ctx.fakeProject = {
  297. rootFolder: [
  298. {
  299. docs: [
  300. { name: 'one.bib', _id: 'aaa' },
  301. { name: 'two.txt', _id: 'bbb' },
  302. ],
  303. folders: [
  304. { docs: [{ name: 'three.bib', _id: 'ccc' }], folders: [] },
  305. ],
  306. },
  307. ],
  308. }
  309. ctx.expectedIds = ['aaa', 'ccc']
  310. })
  311. it('should select the correct docIds', function (ctx) {
  312. const result = ctx.handler._findBibDocIds(ctx.fakeProject)
  313. expect(result).to.deep.equal(ctx.expectedIds)
  314. })
  315. it('should not error with a non array of folders from dirty data', function (ctx) {
  316. ctx.fakeProject.rootFolder[0].folders[0].folders = {}
  317. const result = ctx.handler._findBibDocIds(ctx.fakeProject)
  318. expect(result).to.deep.equal(ctx.expectedIds)
  319. })
  320. })
  321. describe('_findBibFileRefs', function () {
  322. beforeEach(function (ctx) {
  323. ctx.fakeProject = {
  324. rootFolder: [
  325. {
  326. docs: [
  327. { name: 'one.bib', _id: 'aaa' },
  328. { name: 'two.txt', _id: 'bbb' },
  329. ],
  330. fileRefs: [{ name: 'other.bib', _id: 'ddd' }],
  331. folders: [
  332. {
  333. docs: [{ name: 'three.bib', _id: 'ccc' }],
  334. fileRefs: [{ name: 'four.bib', _id: 'ghg' }],
  335. folders: [],
  336. },
  337. ],
  338. },
  339. ],
  340. }
  341. ctx.expectedIds = [
  342. ctx.fakeProject.rootFolder[0].fileRefs[0],
  343. ctx.fakeProject.rootFolder[0].folders[0].fileRefs[0],
  344. ]
  345. })
  346. it('should select the correct docIds', function (ctx) {
  347. const result = ctx.handler._findBibFileRefs(ctx.fakeProject)
  348. expect(result).to.deep.equal(ctx.expectedIds)
  349. })
  350. })
  351. describe('_isFullIndex', function () {
  352. beforeEach(function (ctx) {
  353. ctx.fakeProject = { owner_ref: (ctx.owner_ref = 'owner-ref-123') }
  354. ctx.owner = {
  355. features: {
  356. references: false,
  357. },
  358. }
  359. ctx.UserGetter.getUser = sinon.stub()
  360. ctx.UserGetter.getUser
  361. .withArgs(ctx.owner_ref, { features: true })
  362. .yields(null, ctx.owner)
  363. ctx.call = callback => {
  364. ctx.handler._isFullIndex(ctx.fakeProject, callback)
  365. }
  366. })
  367. describe('with references feature on', function () {
  368. beforeEach(function (ctx) {
  369. ctx.owner.features.references = true
  370. })
  371. it('should return true', function (ctx) {
  372. ctx.call((err, isFullIndex) => {
  373. expect(err).to.equal(null)
  374. expect(isFullIndex).to.equal(true)
  375. })
  376. })
  377. })
  378. describe('with references feature off', function () {
  379. beforeEach(function (ctx) {
  380. ctx.owner.features.references = false
  381. })
  382. it('should return false', function (ctx) {
  383. ctx.call((err, isFullIndex) => {
  384. expect(err).to.equal(null)
  385. expect(isFullIndex).to.equal(false)
  386. })
  387. })
  388. })
  389. describe('with referencesSearch', function () {
  390. beforeEach(function (ctx) {
  391. ctx.owner.features = {
  392. referencesSearch: true,
  393. references: false,
  394. }
  395. })
  396. it('should return true', function (ctx) {
  397. ctx.call((err, isFullIndex) => {
  398. expect(err).to.equal(null)
  399. expect(isFullIndex).to.equal(true)
  400. })
  401. })
  402. })
  403. })
  404. })