Files
overleaf-cep/services/web/test/unit/src/History/HistoryManager.sequential.test.mjs
Andrew Rumble 07c827e9fd Merge pull request #29928 from overleaf/ar-last-infrastructure-conversions
[web] last infrastructure conversions

GitOrigin-RevId: ad1aff9b7df0610ed0303157d9e2c8032f32c02b
2025-11-28 09:05:56 +00:00

381 lines
11 KiB
JavaScript

import { beforeAll, beforeEach, describe, it, vi, expect } from 'vitest'
import sinon from 'sinon'
import mongodb from 'mongodb-legacy'
import {
cleanupTestDatabase,
db,
waitForDb,
} from '../../../../app/src/infrastructure/mongodb.mjs'
const { ObjectId } = mongodb
const MODULE_PATH = '../../../../app/src/Features/History/HistoryManager'
const GLOBAL_BLOBS = [
'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391',
'02426c2b3a484003ca42ed52b374b7907b757d12',
]
describe('HistoryManager', function () {
beforeAll(async function () {
await waitForDb()
})
beforeAll(cleanupTestDatabase)
beforeAll(async function () {
await db.projectHistoryGlobalBlobs.insertMany(
GLOBAL_BLOBS.map(sha => ({
_id: sha,
byteLength: 0,
stringLength: 0,
}))
)
})
beforeEach(async function (ctx) {
ctx.user_id = 'user-id-123'
ctx.historyId = new ObjectId().toString()
ctx.AuthenticationController = {
getLoggedInUserId: sinon.stub().returns(ctx.user_id),
}
ctx.FetchUtils = {
fetchJson: sinon.stub(),
fetchNothing: sinon.stub().resolves(),
}
ctx.projectHistoryUrl = 'http://project_history.example.com'
ctx.v1HistoryUrl = 'http://v1_history.example.com'
ctx.v1HistoryUser = 'system'
ctx.v1HistoryPassword = 'verysecret'
ctx.settings = {
apis: {
filestore: {
url: 'http://filestore.example.com',
},
project_history: {
url: ctx.projectHistoryUrl,
},
v1_history: {
url: ctx.v1HistoryUrl,
user: ctx.v1HistoryUser,
pass: ctx.v1HistoryPassword,
buckets: {
globalBlobs: 'globalBlobs',
projectBlobs: 'projectBlobs',
},
},
},
}
ctx.UserGetter = {
promises: {
getUsersByV1Ids: sinon.stub(),
getUsers: sinon.stub(),
},
}
ctx.project = {
overleaf: {
history: {
id: ctx.historyId,
},
},
}
ctx.ProjectGetter = {
promises: {
getProject: sinon.stub().resolves(ctx.project),
},
}
ctx.HistoryBackupDeletionHandler = {
deleteProject: sinon.stub().resolves(),
}
vi.doMock('../../../../app/src/infrastructure/mongodb', () => ({
ObjectId,
db,
waitForDb,
}))
vi.doMock('@overleaf/fetch-utils', () => ctx.FetchUtils)
vi.doMock('@overleaf/settings', () => ({
default: ctx.settings,
}))
vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
default: ctx.UserGetter,
}))
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
default: ctx.ProjectGetter,
}))
vi.doMock(
'../../../../app/src/Features/History/HistoryBackupDeletionHandler',
() => ({
default: ctx.HistoryBackupDeletionHandler,
})
)
ctx.HistoryManager = (await import(MODULE_PATH)).default
})
describe('getFilestoreBlobURL', function () {
beforeEach(async function (ctx) {
await ctx.HistoryManager.loadGlobalBlobsPromise
})
it('should return a global blob location', function (ctx) {
for (const sha of GLOBAL_BLOBS) {
expect(ctx.HistoryManager.getFilestoreBlobURL('42', sha)).to.equal(
`${ctx.settings.apis.filestore.url}/history/global/hash/${sha}`
)
}
})
it('should return a project blob location for a v1 project', function (ctx) {
const historyId = 42
const sha = '6ddfa0578a67fe5ad6623a8665ec9aafce1eb5ca'
expect(ctx.HistoryManager.getFilestoreBlobURL(historyId, sha)).to.equal(
`${ctx.settings.apis.filestore.url}/history/project/${historyId}/hash/${sha}`
)
})
it('should return a project blob location for a mongo project', function (ctx) {
const historyId = '424242424242424242424242'
const sha = '6ddfa0578a67fe5ad6623a8665ec9aafce1eb5ca'
expect(ctx.HistoryManager.getFilestoreBlobURL(historyId, sha)).to.equal(
`${ctx.settings.apis.filestore.url}/history/project/${historyId}/hash/${sha}`
)
})
})
describe('initializeProject', function () {
beforeEach(function (ctx) {
ctx.settings.apis.project_history.initializeHistoryForNewProjects = true
})
describe('project history returns a successful response', function () {
beforeEach(async function (ctx) {
ctx.FetchUtils.fetchJson.resolves({ project: { id: ctx.historyId } })
ctx.result = await ctx.HistoryManager.promises.initializeProject(
ctx.historyId
)
})
it('should call the project history api', function (ctx) {
ctx.FetchUtils.fetchJson.should.have.been.calledWithMatch(
`${ctx.settings.apis.project_history.url}/project`,
{ method: 'POST' }
)
})
it('should return the overleaf id', function (ctx) {
expect(ctx.result).to.equal(ctx.historyId)
})
})
describe('project history returns a response without the project id', function () {
it('should throw an error', async function (ctx) {
ctx.FetchUtils.fetchJson.resolves({ project: {} })
await expect(
ctx.HistoryManager.promises.initializeProject(ctx.historyId)
).to.be.rejected
})
})
describe('project history errors', function () {
it('should propagate the error', async function (ctx) {
ctx.FetchUtils.fetchJson.rejects(new Error('problem connecting'))
await expect(
ctx.HistoryManager.promises.initializeProject(ctx.historyId)
).to.be.rejected
})
})
})
describe('injectUserDetails', function () {
beforeEach(function (ctx) {
ctx.user1 = {
_id: (ctx.user_id1 = '123456'),
first_name: 'Jane',
last_name: 'Doe',
email: 'jane@example.com',
overleaf: { id: 5011 },
}
ctx.user1_view = {
id: ctx.user_id1,
first_name: 'Jane',
last_name: 'Doe',
email: 'jane@example.com',
}
ctx.user2 = {
_id: (ctx.user_id2 = 'abcdef'),
first_name: 'John',
last_name: 'Doe',
email: 'john@example.com',
}
ctx.user2_view = {
id: ctx.user_id2,
first_name: 'John',
last_name: 'Doe',
email: 'john@example.com',
}
ctx.UserGetter.promises.getUsersByV1Ids.resolves([ctx.user1])
ctx.UserGetter.promises.getUsers.resolves([ctx.user1, ctx.user2])
})
describe('with a diff', function () {
it('should turn user_ids into user objects', async function (ctx) {
const diff = await ctx.HistoryManager.promises.injectUserDetails({
diff: [
{
i: 'foo',
meta: {
users: [ctx.user_id1],
},
},
{
i: 'bar',
meta: {
users: [ctx.user_id2],
},
},
],
})
expect(diff.diff[0].meta.users).to.deep.equal([ctx.user1_view])
expect(diff.diff[1].meta.users).to.deep.equal([ctx.user2_view])
})
it('should handle v1 user ids', async function (ctx) {
const diff = await ctx.HistoryManager.promises.injectUserDetails({
diff: [
{
i: 'foo',
meta: {
users: [5011],
},
},
{
i: 'bar',
meta: {
users: [ctx.user_id2],
},
},
],
})
expect(diff.diff[0].meta.users).to.deep.equal([ctx.user1_view])
expect(diff.diff[1].meta.users).to.deep.equal([ctx.user2_view])
})
it('should leave user objects', async function (ctx) {
const diff = await ctx.HistoryManager.promises.injectUserDetails({
diff: [
{
i: 'foo',
meta: {
users: [ctx.user1_view],
},
},
{
i: 'bar',
meta: {
users: [ctx.user_id2],
},
},
],
})
expect(diff.diff[0].meta.users).to.deep.equal([ctx.user1_view])
expect(diff.diff[1].meta.users).to.deep.equal([ctx.user2_view])
})
it('should handle a binary diff marker', async function (ctx) {
const diff = await ctx.HistoryManager.promises.injectUserDetails({
diff: { binary: true },
})
expect(diff.diff.binary).to.be.true
})
})
describe('with a list of updates', function () {
it('should turn user_ids into user objects', async function (ctx) {
const updates = await ctx.HistoryManager.promises.injectUserDetails({
updates: [
{
fromV: 5,
toV: 8,
meta: {
users: [ctx.user_id1],
},
},
{
fromV: 4,
toV: 5,
meta: {
users: [ctx.user_id2],
},
},
],
})
expect(updates.updates[0].meta.users).to.deep.equal([ctx.user1_view])
expect(updates.updates[1].meta.users).to.deep.equal([ctx.user2_view])
})
it('should leave user objects', async function (ctx) {
const updates = await ctx.HistoryManager.promises.injectUserDetails({
updates: [
{
fromV: 5,
toV: 8,
meta: {
users: [ctx.user1_view],
},
},
{
fromV: 4,
toV: 5,
meta: {
users: [ctx.user_id2],
},
},
],
})
expect(updates.updates[0].meta.users).to.deep.equal([ctx.user1_view])
expect(updates.updates[1].meta.users).to.deep.equal([ctx.user2_view])
})
})
})
describe('deleteProject', function () {
const projectId = new ObjectId()
const historyId = new ObjectId()
beforeEach(async function (ctx) {
await ctx.HistoryManager.promises.deleteProject(projectId, historyId)
})
it('should call the project-history service', async function (ctx) {
expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWith(
`${ctx.projectHistoryUrl}/project/${projectId}`,
{ method: 'DELETE' }
)
})
it('should call the v1-history service', async function (ctx) {
expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWith(
`${ctx.v1HistoryUrl}/projects/${historyId}`,
{
method: 'DELETE',
basicAuth: {
user: ctx.v1HistoryUser,
password: ctx.v1HistoryPassword,
},
}
)
})
it('should call the history-backup-deletion service', async function (ctx) {
expect(
ctx.HistoryBackupDeletionHandler.deleteProject
).to.have.been.calledWith(projectId)
})
})
})