mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 06:03:45 +01:00
[web] enforce size limit on individual files when cloning project (#35211)
* [web] enforce size limit on individual files when cloning project Co-authored-by: Brian Gough <brian.gough@overleaf.com> * [web] improve test coverage * [agents] speed up running frontend tests * [web] gracefully handle project too large error --------- Co-authored-by: Brian Gough <brian.gough@overleaf.com> GitOrigin-RevId: c7b35a3a9688e15a9bb51c61816da82d39e0d29d
This commit is contained in:
committed by
Copybot
co-authored by
Brian Gough
parent
f62bd990d4
commit
84c6a68eae
@@ -1,6 +1,8 @@
|
||||
import { beforeAll, beforeEach, describe, it, vi, expect } from 'vitest'
|
||||
import sinon from 'sinon'
|
||||
import mongodb from 'mongodb-legacy'
|
||||
import { RequestFailedError } from '@overleaf/fetch-utils'
|
||||
import { FileTooLargeError } from '../../../../app/src/Features/Errors/Errors.js'
|
||||
import {
|
||||
cleanupTestDatabase,
|
||||
db,
|
||||
@@ -9,6 +11,12 @@ import {
|
||||
|
||||
const { ObjectId } = mongodb
|
||||
|
||||
// Ensure the module under test and this test file share the same
|
||||
// FileTooLargeError class, so `instanceof` checks line up.
|
||||
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
|
||||
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
|
||||
)
|
||||
|
||||
const MODULE_PATH = '../../../../app/src/Features/History/HistoryManager'
|
||||
|
||||
const GLOBAL_BLOBS = [
|
||||
@@ -40,6 +48,7 @@ describe('HistoryManager', function () {
|
||||
ctx.FetchUtils = {
|
||||
fetchJson: sinon.stub(),
|
||||
fetchNothing: sinon.stub().resolves(),
|
||||
RequestFailedError,
|
||||
}
|
||||
ctx.projectHistoryUrl = 'http://project_history.example.com'
|
||||
ctx.v1HistoryUrl = 'http://v1_history.example.com'
|
||||
@@ -364,6 +373,106 @@ describe('HistoryManager', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('copyBlob', function () {
|
||||
const sourceHistoryId = 'source-history-id'
|
||||
const targetHistoryId = 'target-history-id'
|
||||
const hash = 'abcdef0123456789abcdef0123456789abcdef01'
|
||||
|
||||
it('should call the v1-history service without a size limit', async function (ctx) {
|
||||
await ctx.HistoryManager.promises.copyBlob(
|
||||
sourceHistoryId,
|
||||
targetHistoryId,
|
||||
hash
|
||||
)
|
||||
expect(ctx.FetchUtils.fetchNothing).to.have.been.calledOnce
|
||||
const [url, opts] = ctx.FetchUtils.fetchNothing.getCall(0).args
|
||||
expect(url.toString()).to.equal(
|
||||
`${ctx.v1HistoryUrl}/projects/${targetHistoryId}/blobs/${hash}?copyFrom=${sourceHistoryId}`
|
||||
)
|
||||
expect(opts).to.deep.equal({
|
||||
method: 'POST',
|
||||
basicAuth: {
|
||||
user: ctx.v1HistoryUser,
|
||||
password: ctx.v1HistoryPassword,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass the size limit as a query parameter', async function (ctx) {
|
||||
await ctx.HistoryManager.promises.copyBlob(
|
||||
sourceHistoryId,
|
||||
targetHistoryId,
|
||||
hash,
|
||||
1234
|
||||
)
|
||||
const [url] = ctx.FetchUtils.fetchNothing.getCall(0).args
|
||||
expect(url.searchParams.get('copyFrom')).to.equal(sourceHistoryId)
|
||||
expect(url.searchParams.get('sizeLimit')).to.equal('1234')
|
||||
})
|
||||
|
||||
it('should throw a FileTooLargeError with the size on a 413 response', async function (ctx) {
|
||||
ctx.FetchUtils.fetchNothing.rejects(
|
||||
new RequestFailedError(
|
||||
`${ctx.v1HistoryUrl}/projects/${targetHistoryId}/blobs/${hash}`,
|
||||
{ method: 'POST' },
|
||||
{ status: 413 },
|
||||
JSON.stringify({ size: 9000 })
|
||||
)
|
||||
)
|
||||
await expect(
|
||||
ctx.HistoryManager.promises.copyBlob(
|
||||
sourceHistoryId,
|
||||
targetHistoryId,
|
||||
hash,
|
||||
1234
|
||||
)
|
||||
).to.be.rejected.then(err => {
|
||||
expect(err).to.be.instanceof(FileTooLargeError)
|
||||
expect(err.info.size).to.equal(9000)
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw a FileTooLargeError even if the 413 body is not valid JSON', async function (ctx) {
|
||||
ctx.FetchUtils.fetchNothing.rejects(
|
||||
new RequestFailedError(
|
||||
`${ctx.v1HistoryUrl}/projects/${targetHistoryId}/blobs/${hash}`,
|
||||
{ method: 'POST' },
|
||||
{ status: 413 },
|
||||
'not json'
|
||||
)
|
||||
)
|
||||
await expect(
|
||||
ctx.HistoryManager.promises.copyBlob(
|
||||
sourceHistoryId,
|
||||
targetHistoryId,
|
||||
hash,
|
||||
1234
|
||||
)
|
||||
).to.be.rejected.then(err => {
|
||||
expect(err).to.be.instanceof(FileTooLargeError)
|
||||
expect(err.info.size).to.be.undefined
|
||||
})
|
||||
})
|
||||
|
||||
it('should rethrow non-413 request failures', async function (ctx) {
|
||||
const err = new RequestFailedError(
|
||||
`${ctx.v1HistoryUrl}/projects/${targetHistoryId}/blobs/${hash}`,
|
||||
{ method: 'POST' },
|
||||
{ status: 500 },
|
||||
''
|
||||
)
|
||||
ctx.FetchUtils.fetchNothing.rejects(err)
|
||||
await expect(
|
||||
ctx.HistoryManager.promises.copyBlob(
|
||||
sourceHistoryId,
|
||||
targetHistoryId,
|
||||
hash,
|
||||
1234
|
||||
)
|
||||
).to.be.rejectedWith(err)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteProject', function () {
|
||||
const projectId = new ObjectId()
|
||||
const historyId = new ObjectId()
|
||||
|
||||
@@ -3,8 +3,15 @@ import { beforeEach, describe, it, vi, expect } from 'vitest'
|
||||
import path from 'node:path'
|
||||
import sinon from 'sinon'
|
||||
import mongodb from 'mongodb-legacy'
|
||||
import { FileTooLargeError } from '../../../../app/src/Features/Errors/Errors.js'
|
||||
const { ObjectId } = mongodb
|
||||
|
||||
// Ensure the module under test and this test file share the same
|
||||
// FileTooLargeError class, so `instanceof` checks in the controller line up.
|
||||
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
|
||||
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
|
||||
)
|
||||
|
||||
const MODULE_PATH = path.join(
|
||||
import.meta.dirname,
|
||||
'../../../../app/src/Features/Project/ProjectController'
|
||||
@@ -769,6 +776,53 @@ describe('ProjectController', function () {
|
||||
ctx.ProjectController.cloneProject(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should respond with a 413 when a file is too large to copy', async function (ctx) {
|
||||
const err = new FileTooLargeError('file too large', {
|
||||
path: 'huge.pdf',
|
||||
size: 123456789,
|
||||
})
|
||||
ctx.ProjectDuplicator.promises.duplicate = sinon.stub().rejects(err)
|
||||
await new Promise(resolve => {
|
||||
ctx.res.status = sinon.stub().returns(ctx.res)
|
||||
ctx.res.json = json => {
|
||||
ctx.res.status.should.have.been.calledWith(413)
|
||||
expect(json).to.deep.equal({
|
||||
message: {
|
||||
text: 'file too large to copy',
|
||||
key: 'file_too_large_to_copy',
|
||||
info: { path: 'huge.pdf', size: 123456789 },
|
||||
},
|
||||
})
|
||||
resolve()
|
||||
}
|
||||
ctx.ProjectController.cloneProject(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should forward a FileTooLargeError without a path to next (e.g. project too large)', async function (ctx) {
|
||||
const err = new FileTooLargeError('Project is too large')
|
||||
ctx.ProjectDuplicator.promises.duplicate = sinon.stub().rejects(err)
|
||||
await new Promise(resolve => {
|
||||
const next = forwardedErr => {
|
||||
expect(forwardedErr).to.equal(err)
|
||||
resolve()
|
||||
}
|
||||
ctx.ProjectController.cloneProject(ctx.req, ctx.res, next)
|
||||
})
|
||||
})
|
||||
|
||||
it('should forward other errors to next', async function (ctx) {
|
||||
const err = new Error('boom')
|
||||
ctx.ProjectDuplicator.promises.duplicate = sinon.stub().rejects(err)
|
||||
await new Promise(resolve => {
|
||||
const next = forwardedErr => {
|
||||
expect(forwardedErr).to.equal(err)
|
||||
resolve()
|
||||
}
|
||||
ctx.ProjectController.cloneProject(ctx.req, ctx.res, next)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('newProject', function () {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import sinon from 'sinon'
|
||||
import mongodb from 'mongodb-legacy'
|
||||
import OError from '@overleaf/o-error'
|
||||
import Settings from '@overleaf/settings'
|
||||
|
||||
const { ObjectId } = mongodb
|
||||
|
||||
@@ -340,7 +342,8 @@ describe('ProjectDuplicator', function () {
|
||||
ctx.HistoryManager.promises.copyBlob.should.have.been.calledWith(
|
||||
ctx.project.overleaf.history.id,
|
||||
ctx.newProject.overleaf.history.id,
|
||||
file.hash
|
||||
file.hash,
|
||||
Settings.maxUploadSize
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -447,6 +450,20 @@ describe('ProjectDuplicator', function () {
|
||||
)
|
||||
).to.be.rejectedWith('copy blob error')
|
||||
})
|
||||
|
||||
it('should tag the error with the file path', async function (ctx) {
|
||||
ctx.file0.hash = '500'
|
||||
await expect(
|
||||
ctx.ProjectDuplicator.promises.duplicate(
|
||||
ctx.owner,
|
||||
ctx.project._id,
|
||||
'name'
|
||||
)
|
||||
).to.be.rejected.then(err => {
|
||||
// path is tagged without the leading slash
|
||||
expect(OError.getFullInfo(err).path).to.equal('file0')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is an error', function () {
|
||||
|
||||
Reference in New Issue
Block a user