| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 |
- 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)
- })
- })
- })
|