Files
audiobookshelf/test/server/utils/fileUploadMiddleware.test.js
T

222 lines
6.8 KiB
JavaScript

const { expect } = require('chai')
const express = require('express')
const fs = require('fs')
const http = require('http')
const os = require('os')
const path = require('path')
const sinon = require('sinon')
const Logger = require('../../../server/Logger')
const { fileUploadParser, requireUploadPermission, sweepUploadTempFiles } = require('../../../server/utils/fileUploadMiddleware')
function listTmpFiles(dir) {
if (!fs.existsSync(dir)) return []
return fs.readdirSync(dir).filter((name) => name.startsWith('tmp-'))
}
function sendMultipart(port, urlPath, extraHeaders = {}) {
return new Promise((resolve, reject) => {
const boundary = '----AbsUploadTestBoundary'
const payload = [
`--${boundary}`,
'Content-Disposition: form-data; name="f"; filename="probe.txt"',
'Content-Type: text/plain',
'',
'probe',
`--${boundary}--`,
''
].join('\r\n')
const req = http.request(
{
hostname: '127.0.0.1',
port,
path: urlPath,
method: 'POST',
headers: {
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'Content-Length': Buffer.byteLength(payload),
...extraHeaders
}
},
(res) => {
res.resume()
res.on('end', () => resolve(res.statusCode))
}
)
req.on('error', reject)
req.write(payload)
req.end()
})
}
function listen(app) {
const server = http.createServer(app)
return new Promise((resolve) => {
server.listen(0, '127.0.0.1', () => {
resolve({ server, port: server.address().port })
})
})
}
function closeServer(server) {
return new Promise((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()))
})
}
async function waitFor(predicate, timeoutMs = 2000) {
const started = Date.now()
while (Date.now() - started < timeoutMs) {
if (predicate()) return
await new Promise((resolve) => setTimeout(resolve, 25))
}
throw new Error('Timed out waiting for condition')
}
describe('fileUploadMiddleware', () => {
let metadataDir
let tmpDir
let previousMetadataPath
beforeEach(() => {
previousMetadataPath = global.MetadataPath
metadataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'abs-upload-meta-'))
tmpDir = path.join(metadataDir, 'tmp')
global.MetadataPath = metadataDir
sinon.stub(Logger, 'info')
sinon.stub(Logger, 'warn')
})
afterEach(() => {
sinon.restore()
global.MetadataPath = previousMetadataPath
fs.rmSync(metadataDir, { recursive: true, force: true })
})
describe('requireUploadPermission', () => {
it('returns 403 when the user cannot upload', () => {
const req = { user: { canUpload: false, username: 'guest' } }
const res = { sendStatus: sinon.stub() }
const next = sinon.stub()
requireUploadPermission(req, res, next)
expect(res.sendStatus.calledOnceWithExactly(403)).to.equal(true)
expect(next.called).to.equal(false)
})
it('calls next when the user can upload', () => {
const req = { user: { canUpload: true, username: 'uploader' } }
const res = { sendStatus: sinon.stub() }
const next = sinon.stub()
requireUploadPermission(req, res, next)
expect(next.calledOnce).to.equal(true)
expect(res.sendStatus.called).to.equal(false)
})
})
describe('sweepUploadTempFiles', () => {
it('removes leftover tmp-* files and leaves other names', async () => {
fs.mkdirSync(tmpDir, { recursive: true })
fs.writeFileSync(path.join(tmpDir, 'tmp-1-123'), 'stale')
fs.writeFileSync(path.join(tmpDir, 'tmp-2-456'), 'stale')
fs.writeFileSync(path.join(tmpDir, 'comic-extract-keep'), 'keep')
fs.mkdirSync(path.join(tmpDir, 'tmp-not-a-file'))
await sweepUploadTempFiles()
expect(fs.existsSync(path.join(tmpDir, 'tmp-1-123'))).to.equal(false)
expect(fs.existsSync(path.join(tmpDir, 'tmp-2-456'))).to.equal(false)
expect(fs.existsSync(path.join(tmpDir, 'comic-extract-keep'))).to.equal(true)
expect(fs.existsSync(path.join(tmpDir, 'tmp-not-a-file'))).to.equal(true)
})
it('returns when the temp directory does not exist', async () => {
await sweepUploadTempFiles()
expect(fs.existsSync(tmpDir)).to.equal(false)
})
})
describe('upload parser mounting', () => {
it('does not write tmp-* files for a route without the parser', async () => {
const app = express()
app.use((req, res) => res.sendStatus(404))
const { server, port } = await listen(app)
try {
const status = await sendMultipart(port, '/not-a-route')
expect(status).to.equal(404)
expect(listTmpFiles(tmpDir)).to.deep.equal([])
} finally {
await closeServer(server)
}
})
it('does not write tmp-* files when permission is rejected before parsing', async () => {
const app = express()
app.use((req, res, next) => {
req.user = { canUpload: false, username: 'guest' }
next()
})
app.post('/upload', requireUploadPermission, fileUploadParser, (req, res) => {
res.sendStatus(200)
})
const { server, port } = await listen(app)
try {
const status = await sendMultipart(port, '/upload')
expect(status).to.equal(403)
expect(listTmpFiles(tmpDir)).to.deep.equal([])
} finally {
await closeServer(server)
}
})
it('removes the temp file when the handler rejects without moving it', async () => {
const app = express()
app.use((req, res, next) => {
req.user = { canUpload: true, username: 'uploader' }
next()
})
app.post('/upload', requireUploadPermission, fileUploadParser, (req, res) => {
res.status(400).send('Invalid request no file or url')
})
const { server, port } = await listen(app)
try {
const status = await sendMultipart(port, '/upload')
expect(status).to.equal(400)
await waitFor(() => listTmpFiles(tmpDir).length === 0)
} finally {
await closeServer(server)
}
})
it('keeps a moved file and leaves no tmp-* leftover', async () => {
const dest = path.join(metadataDir, 'moved.txt')
const app = express()
app.use((req, res, next) => {
req.user = { canUpload: true, username: 'uploader' }
next()
})
app.post('/upload', requireUploadPermission, fileUploadParser, async (req, res) => {
await req.files.f.mv(dest)
res.sendStatus(200)
})
const { server, port } = await listen(app)
try {
const status = await sendMultipart(port, '/upload')
expect(status).to.equal(200)
expect(fs.readFileSync(dest, 'utf8')).to.equal('probe')
await waitFor(() => listTmpFiles(tmpDir).length === 0)
} finally {
await closeServer(server)
}
})
})
})