Files
overleaf-cep/services/filestore/test/unit/js/FileController.test.js
Andrew Rumble 90cf4b6a0a Merge pull request #29841 from overleaf/ar-convert-filestore-to-esm
[filestore] convert to ES modules

GitOrigin-RevId: 404905973548bb6e437fff66b368e87be8249b73
2025-12-05 09:05:35 +00:00

258 lines
6.7 KiB
JavaScript

import sinon from 'sinon'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Errors from '../../../app/js/Errors.js'
const modulePath = '../../../app/js/FileController.js'
describe('FileController', () => {
let FileHandler, LocalFileWriter, FileController, req, res, next, stream
const settings = {
s3: {
buckets: {
template_files: 'template_files',
},
},
}
const fileSize = 1234
const fileStream = {
destroy() {},
}
const projectId = 'projectId'
const fileId = 'file_id'
const bucket = 'template_files'
const key = `${projectId}/${fileId}`
const error = new Error('incorrect utensil')
beforeEach(async () => {
FileHandler = {
getFile: sinon.stub().yields(null, fileStream),
getFileSize: sinon.stub().yields(null, fileSize),
insertFile: sinon.stub().yields(),
getRedirectUrl: sinon.stub().yields(null, null),
}
LocalFileWriter = {}
stream = {
pipeline: sinon.stub(),
}
vi.doMock('../../../app/js/LocalFileWriter', () => ({
default: LocalFileWriter,
}))
vi.doMock('../../../app/js/FileHandler', () => ({
default: FileHandler,
}))
vi.doMock('../../../app/js/Errors', () => ({
default: Errors,
}))
vi.doMock('stream', () => stream)
vi.doMock('@overleaf/settings', () => ({
default: settings,
}))
vi.doMock('@overleaf/metrics', () => ({
default: {
inc() {},
},
}))
FileController = (await import(modulePath)).default
req = {
key,
bucket,
project_id: projectId,
query: {},
params: {
project_id: projectId,
file_id: fileId,
},
headers: {},
requestLogger: {
setMessage: sinon.stub(),
addFields: sinon.stub(),
},
}
res = {
set: sinon.stub().returnsThis(),
sendStatus: sinon.stub().returnsThis(),
status: sinon.stub().returnsThis(),
}
next = sinon.stub()
})
describe('getFile', () => {
it('should try and get a redirect url first', () => {
FileController.getFile(req, res, next)
expect(FileHandler.getRedirectUrl).to.have.been.calledWith(bucket, key)
})
it('should pipe the stream', () => {
FileController.getFile(req, res, next)
expect(stream.pipeline).to.have.been.calledWith(fileStream, res)
})
it('should send a 200 if the cacheWarm param is true', async () => {
req.query.cacheWarm = true
await new Promise(resolve => {
res.sendStatus = statusCode => {
expect(statusCode).to.equal(200)
resolve()
}
FileController.getFile(req, res, next)
})
})
it('should send an error if there is a problem', () => {
FileHandler.getFile.yields(error)
FileController.getFile(req, res, next)
expect(next).to.have.been.calledWith(error)
})
describe('with a redirect url', () => {
const redirectUrl = 'https://wombat.potato/giraffe'
beforeEach(() => {
FileHandler.getRedirectUrl.yields(null, redirectUrl)
res.redirect = sinon.stub()
})
it('should redirect', () => {
FileController.getFile(req, res, next)
expect(res.redirect).to.have.been.calledWith(redirectUrl)
})
it('should not get a file stream', () => {
FileController.getFile(req, res, next)
expect(FileHandler.getFile).not.to.have.been.called
})
describe('when there is an error getting the redirect url', () => {
beforeEach(() => {
FileHandler.getRedirectUrl.yields(new Error('wombat herding error'))
})
it('should not redirect', () => {
FileController.getFile(req, res, next)
expect(res.redirect).not.to.have.been.called
})
it('should not return an error', () => {
FileController.getFile(req, res, next)
expect(next).not.to.have.been.called
})
it('should proxy the file', () => {
FileController.getFile(req, res, next)
expect(FileHandler.getFile).to.have.been.calledWith(bucket, key)
})
})
})
describe('with a range header', () => {
let expectedOptions
beforeEach(() => {
expectedOptions = {
bucket,
key,
format: undefined,
style: undefined,
}
})
it('should pass range options to FileHandler', () => {
req.headers.range = 'bytes=0-8'
expectedOptions.start = 0
expectedOptions.end = 8
FileController.getFile(req, res, next)
expect(FileHandler.getFile).to.have.been.calledWith(
bucket,
key,
expectedOptions
)
})
it('should ignore an invalid range header', () => {
req.headers.range = 'potato'
FileController.getFile(req, res, next)
expect(FileHandler.getFile).to.have.been.calledWith(
bucket,
key,
expectedOptions
)
})
it("should ignore any type other than 'bytes'", () => {
req.headers.range = 'wombats=0-8'
FileController.getFile(req, res, next)
expect(FileHandler.getFile).to.have.been.calledWith(
bucket,
key,
expectedOptions
)
})
})
})
describe('getFileHead', () => {
it('should return the file size in a Content-Length header', async () => {
await new Promise(resolve => {
res.end = () => {
expect(res.status).to.have.been.calledWith(200)
expect(res.set).to.have.been.calledWith('Content-Length', fileSize)
resolve()
}
FileController.getFileHead(req, res, next)
})
})
it('should return a 404 is the file is not found', async () => {
await new Promise(resolve => {
FileHandler.getFileSize.yields(
new Errors.NotFoundError({ message: 'not found', info: {} })
)
res.sendStatus = code => {
expect(code).to.equal(404)
resolve()
}
FileController.getFileHead(req, res, next)
})
})
it('should send an error on internal errors', () => {
FileHandler.getFileSize.yields(error)
FileController.getFileHead(req, res, next)
expect(next).to.have.been.calledWith(error)
})
})
describe('insertFile', () => {
it('should send bucket name key and res to FileHandler', async () => {
await new Promise(resolve => {
res.sendStatus = code => {
expect(FileHandler.insertFile).to.have.been.calledWith(
bucket,
key,
req
)
expect(code).to.equal(200)
resolve()
}
FileController.insertFile(req, res, next)
})
})
})
})