import { vi, describe, beforeEach, it } from 'vitest' import sinon from 'sinon' import path from 'node:path' const modulePath = path.join( import.meta.dirname, '../../../app/js/ProjectPersistenceManager' ) describe('ProjectPersistenceManager', () => { beforeEach(async ctx => { ctx.fsPromises = { statfs: sinon.stub(), } vi.doMock('@overleaf/metrics', () => ({ default: (ctx.Metrics = { gauge: sinon.stub() }), })) vi.doMock('../../../app/js/UrlCache', () => ({ default: (ctx.UrlCache = {}), })) vi.doMock( '../../../app/js/HistoryResourceWriter', () => (ctx.HistoryResourceWriter = { clearCacheCb: sinon.stub().yields(null), }) ) vi.doMock('../../../app/js/CompileManager', () => ({ default: (ctx.CompileManager = {}), })) vi.doMock('fs', () => ({ default: { promises: ctx.fsPromises }, })) vi.doMock('@overleaf/settings', () => ({ default: (ctx.settings = { project_cache_length_ms: 1000, path: { compilesDir: '/compiles', outputDir: '/output', clsiCacheDir: '/cache', }, }), })) ctx.ProjectPersistenceManager = (await import(modulePath)).default ctx.callback = sinon.stub() ctx.project_id = 'project-id-123' return (ctx.user_id = '1234') }) describe('refreshExpiryTimeout', () => { it('should leave expiry alone if plenty of disk', async ctx => { await new Promise((resolve, reject) => { ctx.fsPromises.statfs.resolves({ blocks: 100, bsize: 1, bavail: 40, }) ctx.ProjectPersistenceManager.refreshExpiryTimeout(() => { ctx.Metrics.gauge.should.have.been.calledWith( 'disk_available_percent', 40 ) ctx.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal( ctx.settings.project_cache_length_ms ) resolve() }) }) }) it('should drop EXPIRY_TIMEOUT 10% if low disk usage', async ctx => { await new Promise((resolve, reject) => { ctx.fsPromises.statfs.resolves({ blocks: 100, bsize: 1, bavail: 5, }) ctx.ProjectPersistenceManager.refreshExpiryTimeout(() => { ctx.Metrics.gauge.should.have.been.calledWith( 'disk_available_percent', 5 ) ctx.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(900) resolve() }) }) }) it('should not drop EXPIRY_TIMEOUT to below 50% of project_cache_length_ms', async ctx => { await new Promise((resolve, reject) => { ctx.fsPromises.statfs.resolves({ blocks: 100, bsize: 1, bavail: 5, }) ctx.ProjectPersistenceManager.EXPIRY_TIMEOUT = 500 ctx.ProjectPersistenceManager.refreshExpiryTimeout(() => { ctx.Metrics.gauge.should.have.been.calledWith( 'disk_available_percent', 5 ) ctx.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(500) resolve() }) }) }) it('should not modify EXPIRY_TIMEOUT if there is an error getting disk values', async ctx => { await new Promise((resolve, reject) => { ctx.fsPromises.statfs.rejects(new Error()) ctx.ProjectPersistenceManager.refreshExpiryTimeout(() => { ctx.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(1000) resolve() }) }) }) }) describe('clearExpiredProjects', () => { beforeEach(ctx => { ctx.project_ids = ['project-id-1', 'project-id-2'] ctx.ProjectPersistenceManager._findExpiredProjectIds = sinon .stub() .callsArgWith(0, null, ctx.project_ids) ctx.ProjectPersistenceManager.clearProjectFromCache = sinon .stub() .callsArg(2) ctx.CompileManager.clearExpiredProjects = sinon.stub().callsArg(1) return ctx.ProjectPersistenceManager.clearExpiredProjects(ctx.callback) }) it('should clear each expired project', ctx => { return Array.from(ctx.project_ids).map(projectId => ctx.ProjectPersistenceManager.clearProjectFromCache .calledWith(projectId) .should.equal(true) ) }) return it('should call the callback', ctx => { return ctx.callback.called.should.equal(true) }) }) return describe('clearProject', () => { beforeEach(ctx => { ctx.ProjectPersistenceManager._clearProjectFromDatabase = sinon .stub() .callsArg(1) ctx.UrlCache.clearProject = sinon.stub().callsArg(2) ctx.CompileManager.clearProject = sinon.stub().callsArg(2) return ctx.ProjectPersistenceManager.clearProject( ctx.project_id, ctx.user_id, ctx.callback ) }) it('should clear the project from the database', ctx => { return ctx.ProjectPersistenceManager._clearProjectFromDatabase .calledWith(ctx.project_id) .should.equal(true) }) it('should clear the history cache', ctx => { ctx.HistoryResourceWriter.clearCacheCb.should.have.been.calledWith( ctx.project_id, ctx.user_id ) }) it('should clear all the cached Urls for the project', ctx => { return ctx.UrlCache.clearProject .calledWith(ctx.project_id) .should.equal(true) }) it('should clear the project compile folder', ctx => { return ctx.CompileManager.clearProject .calledWith(ctx.project_id, ctx.user_id) .should.equal(true) }) return it('should call the callback', ctx => { return ctx.callback.called.should.equal(true) }) }) })