Merge pull request #28945 from overleaf/mj-tear-down-server-side-referencing

[web] Tear down server side referencing

GitOrigin-RevId: 37feac39cc7bf219a2cbc463191163534434f267
This commit is contained in:
Mathias Jakobsen
2025-10-28 09:04:53 +00:00
committed by Copybot
parent 45ad16445f
commit 43a80ef8a5
6 changed files with 51 additions and 806 deletions
@@ -34,8 +34,7 @@ describe('LinkedFilesController', function () {
ctx.SessionManager = {
getLoggedInUserId: sinon.stub().returns(ctx.userId),
}
ctx.EditorRealTimeController = {}
ctx.ReferencesHandler = {}
ctx.EditorRealTimeController = { emitToRoom: sinon.stub() }
ctx.UrlAgent = {}
ctx.ProjectFileAgent = {}
ctx.ProjectOutputFileAgent = {}
@@ -74,13 +73,6 @@ describe('LinkedFilesController', function () {
})
)
vi.doMock(
'../../../../app/src/Features/References/ReferencesHandler',
() => ({
default: ctx.ReferencesHandler,
})
)
vi.doMock('../../../../app/src/Features/LinkedFiles/UrlAgent', () => ({
default: ctx.UrlAgent,
}))
@@ -200,5 +192,38 @@ describe('LinkedFilesController', function () {
ctx.LinkedFilesController.refreshLinkedFile(ctx.req, ctx.res, ctx.next)
})
})
describe('when bib file re-indexing is required', function () {
const clientId = 'client-id'
beforeEach(function (ctx) {
ctx.req.body.shouldReindexReferences = true
ctx.req.body.clientId = clientId
})
it('informs clients to re-index bib references', async function (ctx) {
await new Promise(resolve => {
ctx.next = sinon.stub().callsFake(() => resolve('unexpected error'))
ctx.res = {
json: () => {
expect(
ctx.EditorRealTimeController.emitToRoom
).to.have.been.calledWith(
ctx.projectId,
'references:keys:updated',
[],
true,
clientId
)
resolve()
},
}
ctx.LinkedFilesController.refreshLinkedFile(
ctx.req,
ctx.res,
ctx.next
)
})
})
})
})
})
@@ -15,16 +15,6 @@ describe('ReferencesController', function () {
}),
}))
vi.doMock(
'../../../../app/src/Features/References/ReferencesHandler',
() => ({
default: (ctx.ReferencesHandler = {
index: sinon.stub(),
indexAll: sinon.stub(),
}),
})
)
vi.doMock(
'../../../../app/src/Features/Editor/EditorRealTimeController',
() => ({
@@ -45,16 +35,15 @@ describe('ReferencesController', function () {
ctx.res.json = sinon.stub()
ctx.res.sendStatus = sinon.stub()
ctx.next = sinon.stub()
ctx.fakeResponseData = {
ctx.expectedResponseData = {
projectId: ctx.projectId,
keys: ['one', 'two', 'three'],
keys: [],
}
})
describe('indexAll', function () {
beforeEach(function (ctx) {
ctx.req.body = { shouldBroadcast: false }
ctx.ReferencesHandler.indexAll.callsArgWith(1, null, ctx.fakeResponseData)
ctx.call = callback => {
ctx.controller.indexAll(ctx.req, ctx.res, ctx.next)
return callback()
@@ -72,23 +61,11 @@ describe('ReferencesController', function () {
})
})
it('should return data', async function (ctx) {
it('should return expected empty data', async function (ctx) {
await new Promise(resolve => {
ctx.call(() => {
ctx.res.json.callCount.should.equal(1)
ctx.res.json.calledWith(ctx.fakeResponseData).should.equal(true)
resolve()
})
})
})
it('should call ReferencesHandler.indexAll', async function (ctx) {
await new Promise(resolve => {
ctx.call(() => {
ctx.ReferencesHandler.indexAll.callCount.should.equal(1)
ctx.ReferencesHandler.indexAll
.calledWith(ctx.projectId)
.should.equal(true)
ctx.res.json.calledWith(ctx.expectedResponseData).should.equal(true)
resolve()
})
})
@@ -96,7 +73,6 @@ describe('ReferencesController', function () {
describe('when shouldBroadcast is true', function () {
beforeEach(function (ctx) {
ctx.ReferencesHandler.index.callsArgWith(2, null, ctx.fakeResponseData)
ctx.req.body.shouldBroadcast = true
})
@@ -120,11 +96,11 @@ describe('ReferencesController', function () {
})
})
it('should still return data', async function (ctx) {
it('should still return empty data', async function (ctx) {
await new Promise(resolve => {
ctx.call(() => {
ctx.res.json.callCount.should.equal(1)
ctx.res.json.calledWith(ctx.fakeResponseData).should.equal(true)
ctx.res.json.calledWith(ctx.expectedResponseData).should.equal(true)
resolve()
})
})
@@ -133,7 +109,6 @@ describe('ReferencesController', function () {
describe('when shouldBroadcast is false', function () {
beforeEach(function (ctx) {
ctx.ReferencesHandler.index.callsArgWith(2, null, ctx.fakeResponseData)
ctx.req.body.shouldBroadcast = false
})
@@ -157,57 +132,15 @@ describe('ReferencesController', function () {
})
})
it('should still return data', async function (ctx) {
it('should still return empty data', async function (ctx) {
await new Promise(resolve => {
ctx.call(() => {
ctx.res.json.callCount.should.equal(1)
ctx.res.json.calledWith(ctx.fakeResponseData).should.equal(true)
ctx.res.json.calledWith(ctx.expectedResponseData).should.equal(true)
resolve()
})
})
})
})
})
describe('there is no data', function () {
beforeEach(function (ctx) {
ctx.ReferencesHandler.indexAll.callsArgWith(1)
ctx.call = callback => {
ctx.controller.indexAll(ctx.req, ctx.res, ctx.next)
callback()
}
})
it('should not call EditorRealTimeController.emitToRoom', async function (ctx) {
await new Promise(resolve => {
ctx.call(() => {
ctx.EditorRealTimeController.emitToRoom.callCount.should.equal(0)
resolve()
})
})
})
it('should not produce an error', async function (ctx) {
await new Promise(resolve => {
ctx.call(() => {
ctx.res.sendStatus.callCount.should.equal(0)
ctx.res.sendStatus.calledWith(500).should.equal(false)
ctx.res.sendStatus.calledWith(400).should.equal(false)
resolve()
})
})
})
it('should send a response with an empty keys list', async function (ctx) {
await new Promise(resolve => {
ctx.call(() => {
ctx.res.json.called.should.equal(true)
ctx.res.json
.calledWith({ projectId: ctx.projectId, keys: [] })
.should.equal(true)
resolve()
})
})
})
})
})
@@ -1,444 +0,0 @@
import { expect, vi } from 'vitest'
import sinon from 'sinon'
import Errors from '../../../../app/src/Features/Errors/Errors.js'
const modulePath =
'../../../../app/src/Features/References/ReferencesHandler.mjs'
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
)
describe('ReferencesHandler', function () {
beforeEach(async function (ctx) {
ctx.projectId = '222'
ctx.historyId = 42
ctx.fakeProject = {
_id: ctx.projectId,
owner_ref: (ctx.fakeOwner = {
_id: 'some_owner',
features: {
references: false,
},
}),
rootFolder: [
{
docs: [
{ name: 'one.bib', _id: 'aaa' },
{ name: 'two.txt', _id: 'bbb' },
],
folders: [
{
docs: [{ name: 'three.bib', _id: 'ccc' }],
fileRefs: [
{ name: 'four.bib', _id: 'fff', hash: 'abc' },
{ name: 'five.bib', _id: 'ggg', hash: 'def' },
],
folders: [],
},
],
},
],
overleaf: { history: { id: ctx.historyId } },
}
ctx.docIds = ['aaa', 'ccc']
vi.doMock('@overleaf/settings', () => ({
default: (ctx.settings = {
apis: {
references: { url: 'http://some.url/references' },
docstore: { url: 'http://some.url/docstore' },
filestore: { url: 'http://some.url/filestore' },
project_history: { url: 'http://project-history.local' },
},
}),
}))
vi.doMock('request', () => ({
default: (ctx.request = {
get: sinon.stub(),
post: sinon.stub(),
}),
}))
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
default: (ctx.ProjectGetter = {
getProject: sinon.stub().callsArgWith(2, null, ctx.fakeProject),
}),
}))
vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
default: (ctx.UserGetter = {
getUser: sinon.stub(),
}),
}))
vi.doMock(
'../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler',
() => ({
default: (ctx.DocumentUpdaterHandler = {
flushDocToMongo: sinon.stub().callsArgWith(2, null),
}),
})
)
vi.doMock('../../../../app/src/infrastructure/Features', () => ({
default: (ctx.Features = {
hasFeature: sinon.stub().returns(true),
}),
}))
ctx.handler = (await import(modulePath)).default
ctx.fakeResponseData = {
projectId: ctx.projectId,
keys: ['k1', 'k2'],
}
})
describe('indexAll', function () {
beforeEach(function (ctx) {
sinon.stub(ctx.handler, '_findBibDocIds').returns(['aaa', 'ccc'])
sinon.stub(ctx.handler, '_findBibFileRefs').returns([
{ _id: 'fff', hash: 'abc' },
{ _id: 'ggg', hash: 'def' },
])
sinon.stub(ctx.handler, '_isFullIndex').callsArgWith(1, null, true)
ctx.request.post.callsArgWith(
1,
null,
{ statusCode: 200 },
ctx.fakeResponseData
)
return (ctx.call = callback => {
return ctx.handler.indexAll(ctx.projectId, callback)
})
})
it('should call _findBibDocIds', async function (ctx) {
await new Promise(resolve => {
return ctx.call((err, data) => {
expect(err).to.be.null
ctx.handler._findBibDocIds.callCount.should.equal(1)
ctx.handler._findBibDocIds
.calledWith(ctx.fakeProject)
.should.equal(true)
return resolve()
})
})
})
it('should call _findBibFileRefs', async function (ctx) {
await new Promise(resolve => {
return ctx.call((err, data) => {
expect(err).to.be.null
ctx.handler._findBibDocIds.callCount.should.equal(1)
ctx.handler._findBibDocIds
.calledWith(ctx.fakeProject)
.should.equal(true)
return resolve()
})
})
})
it('should call DocumentUpdaterHandler.flushDocToMongo', async function (ctx) {
await new Promise(resolve => {
return ctx.call((err, data) => {
expect(err).to.be.null
ctx.DocumentUpdaterHandler.flushDocToMongo.callCount.should.equal(2)
return resolve()
})
})
})
it('should make a request to references service', async function (ctx) {
await new Promise(resolve => {
return ctx.call((err, data) => {
expect(err).to.be.null
ctx.request.post.callCount.should.equal(1)
const arg = ctx.request.post.firstCall.args[0]
expect(arg.json).to.have.all.keys(
'docUrls',
'sourceURLs',
'fullIndex'
)
expect(arg.json.docUrls.length).to.equal(4)
expect(arg.json.docUrls).to.deep.equal([
`${ctx.settings.apis.docstore.url}/project/${ctx.projectId}/doc/aaa/raw`,
`${ctx.settings.apis.docstore.url}/project/${ctx.projectId}/doc/ccc/raw`,
`${ctx.settings.apis.project_history.url}/project/${ctx.historyId}/blob/abc`,
`${ctx.settings.apis.project_history.url}/project/${ctx.historyId}/blob/def`,
])
expect(arg.json.sourceURLs.length).to.equal(4)
expect(arg.json.sourceURLs).to.deep.equal([
{
url: `${ctx.settings.apis.docstore.url}/project/${ctx.projectId}/doc/aaa/raw`,
},
{
url: `${ctx.settings.apis.docstore.url}/project/${ctx.projectId}/doc/ccc/raw`,
},
{
url: `${ctx.settings.apis.project_history.url}/project/${ctx.historyId}/blob/abc`,
},
{
url: `${ctx.settings.apis.project_history.url}/project/${ctx.historyId}/blob/def`,
},
])
expect(arg.json.fullIndex).to.equal(true)
return resolve()
})
})
})
it('should not produce an error', async function (ctx) {
await new Promise(resolve => {
return ctx.call((err, data) => {
expect(err).to.equal(null)
return resolve()
})
})
})
it('should return data', async function (ctx) {
await new Promise(resolve => {
return ctx.call((err, data) => {
expect(err).to.be.null
expect(data).to.not.equal(null)
expect(data).to.not.equal(undefined)
expect(data).to.equal(ctx.fakeResponseData)
return resolve()
})
})
})
describe('when ProjectGetter.getProject produces an error', function () {
beforeEach(function (ctx) {
ctx.ProjectGetter.getProject.callsArgWith(2, new Error('woops'))
})
it('should produce an error', async function (ctx) {
await new Promise(resolve => {
ctx.call((err, data) => {
expect(err).to.not.equal(null)
expect(err).to.be.instanceof(Error)
expect(data).to.equal(undefined)
resolve()
})
})
})
it('should not send request', async function (ctx) {
await new Promise(resolve => {
ctx.call(() => {
ctx.request.post.callCount.should.equal(0)
resolve()
})
})
})
})
describe('when ProjectGetter.getProject returns null', function () {
beforeEach(function (ctx) {
ctx.ProjectGetter.getProject.callsArgWith(2, null)
})
it('should produce an error', async function (ctx) {
await new Promise(resolve => {
ctx.call((err, data) => {
expect(err).to.not.equal(null)
expect(err).to.be.instanceof(Errors.NotFoundError)
expect(data).to.equal(undefined)
resolve()
})
})
})
it('should not send request', async function (ctx) {
await new Promise(resolve => {
ctx.call(() => {
ctx.request.post.callCount.should.equal(0)
resolve()
})
})
})
})
describe('when _isFullIndex produces an error', function () {
beforeEach(function (ctx) {
ctx.ProjectGetter.getProject.callsArgWith(2, null, ctx.fakeProject)
ctx.handler._isFullIndex.callsArgWith(1, new Error('woops'))
})
it('should produce an error', async function (ctx) {
await new Promise(resolve => {
ctx.call((err, data) => {
expect(err).to.not.equal(null)
expect(err).to.be.instanceof(Error)
expect(data).to.equal(undefined)
resolve()
})
})
})
it('should not send request', async function (ctx) {
await new Promise(resolve => {
ctx.call(() => {
ctx.request.post.callCount.should.equal(0)
resolve()
})
})
})
})
describe('when flushDocToMongo produces an error', function () {
beforeEach(function (ctx) {
ctx.ProjectGetter.getProject.callsArgWith(2, null, ctx.fakeProject)
ctx.handler._isFullIndex.callsArgWith(1, false)
ctx.DocumentUpdaterHandler.flushDocToMongo.callsArgWith(
2,
new Error('woops')
)
})
it('should produce an error', async function (ctx) {
await new Promise(resolve => {
ctx.call((err, data) => {
expect(err).to.not.equal(null)
expect(err).to.be.instanceof(Error)
expect(data).to.equal(undefined)
resolve()
})
})
})
it('should not send request', async function (ctx) {
await new Promise(resolve => {
ctx.call(() => {
ctx.request.post.callCount.should.equal(0)
resolve()
})
})
})
})
})
describe('_findBibDocIds', function () {
beforeEach(function (ctx) {
ctx.fakeProject = {
rootFolder: [
{
docs: [
{ name: 'one.bib', _id: 'aaa' },
{ name: 'two.txt', _id: 'bbb' },
],
folders: [
{ docs: [{ name: 'three.bib', _id: 'ccc' }], folders: [] },
],
},
],
}
ctx.expectedIds = ['aaa', 'ccc']
})
it('should select the correct docIds', function (ctx) {
const result = ctx.handler._findBibDocIds(ctx.fakeProject)
expect(result).to.deep.equal(ctx.expectedIds)
})
it('should not error with a non array of folders from dirty data', function (ctx) {
ctx.fakeProject.rootFolder[0].folders[0].folders = {}
const result = ctx.handler._findBibDocIds(ctx.fakeProject)
expect(result).to.deep.equal(ctx.expectedIds)
})
})
describe('_findBibFileRefs', function () {
beforeEach(function (ctx) {
ctx.fakeProject = {
rootFolder: [
{
docs: [
{ name: 'one.bib', _id: 'aaa' },
{ name: 'two.txt', _id: 'bbb' },
],
fileRefs: [{ name: 'other.bib', _id: 'ddd' }],
folders: [
{
docs: [{ name: 'three.bib', _id: 'ccc' }],
fileRefs: [{ name: 'four.bib', _id: 'ghg' }],
folders: [],
},
],
},
],
}
ctx.expectedIds = [
ctx.fakeProject.rootFolder[0].fileRefs[0],
ctx.fakeProject.rootFolder[0].folders[0].fileRefs[0],
]
})
it('should select the correct docIds', function (ctx) {
const result = ctx.handler._findBibFileRefs(ctx.fakeProject)
expect(result).to.deep.equal(ctx.expectedIds)
})
})
describe('_isFullIndex', function () {
beforeEach(function (ctx) {
ctx.fakeProject = { owner_ref: (ctx.owner_ref = 'owner-ref-123') }
ctx.owner = {
features: {
references: false,
},
}
ctx.UserGetter.getUser = sinon.stub()
ctx.UserGetter.getUser
.withArgs(ctx.owner_ref, { features: true })
.yields(null, ctx.owner)
ctx.call = callback => {
ctx.handler._isFullIndex(ctx.fakeProject, callback)
}
})
describe('with references feature on', function () {
beforeEach(function (ctx) {
ctx.owner.features.references = true
})
it('should return true', function (ctx) {
ctx.call((err, isFullIndex) => {
expect(err).to.equal(null)
expect(isFullIndex).to.equal(true)
})
})
})
describe('with references feature off', function () {
beforeEach(function (ctx) {
ctx.owner.features.references = false
})
it('should return false', function (ctx) {
ctx.call((err, isFullIndex) => {
expect(err).to.equal(null)
expect(isFullIndex).to.equal(false)
})
})
})
describe('with referencesSearch', function () {
beforeEach(function (ctx) {
ctx.owner.features = {
referencesSearch: true,
references: false,
}
})
it('should return true', function (ctx) {
ctx.call((err, isFullIndex) => {
expect(err).to.equal(null)
expect(isFullIndex).to.equal(true)
})
})
})
})
})