diff --git a/src/app.js b/src/app.js index a46dcec..94c8baa 100644 --- a/src/app.js +++ b/src/app.js @@ -13,6 +13,14 @@ import { deleteIfExists } from './storage.js'; import { createJob, getJobByUuid } from './jobs/jobRepository.js'; import { outputPath } from './storage.js'; +function isValidQuality(targetFormat, quality) { + if (quality === null || quality === undefined) return true; + if (!Number.isInteger(quality)) return false; + if (targetFormat === 'gif') return false; + if (targetFormat === 'png') return quality >= 0 && quality <= 9; + return quality >= 1 && quality <= 100; +} + let convertersRegistered = false; function registerAllConverters() { @@ -73,6 +81,21 @@ export function createApp(config, pool) { return res.status(400).json({ error: 'targetFormats must have one entry per uploaded file' }); } + let qualities; + try { + qualities = JSON.parse(req.body.qualities ?? '[]'); + } catch { + return res.status(400).json({ error: 'qualities must be a JSON array' }); + } + + if (!Array.isArray(qualities)) { + return res.status(400).json({ error: 'qualities must be a JSON array' }); + } + + if (qualities.length > 0 && qualities.length !== req.files.length) { + return res.status(400).json({ error: 'qualities must have one entry per uploaded file, or be omitted' }); + } + const results = []; for (let i = 0; i < req.files.length; i += 1) { const file = req.files[i]; @@ -97,6 +120,16 @@ export function createApp(config, pool) { continue; } + const requestedQuality = qualities[i] ?? null; + if (!isValidQuality(targetFormat, requestedQuality)) { + await deleteIfExists(file.path); + results.push({ + file: file.originalname, + error: `Invalid quality for target format ${targetFormat}`, + }); + continue; + } + const expiresAt = new Date(Date.now() + config.retentionHours * 3600 * 1000); await createJob(pool, { uuid, @@ -108,6 +141,7 @@ export function createApp(config, pool) { inputMimeType: mime, inputSizeBytes: file.size, expiresAt, + quality: requestedQuality, }); results.push({ file: file.originalname, id: uuid, status: 'pending' }); diff --git a/test/api/jobs.test.js b/test/api/jobs.test.js index 72501f0..7622b2e 100644 --- a/test/api/jobs.test.js +++ b/test/api/jobs.test.js @@ -67,6 +67,61 @@ describe('POST /api/jobs', () => { expect(job.originalFilename).toBe('photo.png'); expect(job.inputMimeType).toBe('image/png'); expect(job.inputSizeBytes).toBeGreaterThan(0); + expect(job.quality).toBeNull(); + }); + + it('creates a pending job with a quality level for a supported format', async () => { + const fixturePath = path.join(import.meta.dirname, '..', 'fixtures', 'sample.png'); + + const response = await request(app) + .post('/api/jobs') + .field('targetFormats', JSON.stringify(['jpg'])) + .field('qualities', JSON.stringify([45])) + .attach('files', fixturePath, 'photo.png'); + + expect(response.status).toBe(201); + expect(response.body.jobs[0].status).toBe('pending'); + + const job = await getJobByUuid(pool, response.body.jobs[0].id); + expect(job.quality).toBe(45); + }); + + it('rejects an out-of-range quality without failing the whole batch', async () => { + const fixturePath = path.join(import.meta.dirname, '..', 'fixtures', 'sample.png'); + + const response = await request(app) + .post('/api/jobs') + .field('targetFormats', JSON.stringify(['jpg'])) + .field('qualities', JSON.stringify([500])) + .attach('files', fixturePath, 'photo.png'); + + expect(response.status).toBe(201); + expect(response.body.jobs[0].error).toMatch(/Invalid quality/); + }); + + it('rejects a quality value for a format that does not support one', async () => { + const fixturePath = path.join(import.meta.dirname, '..', 'fixtures', 'sample.png'); + + const response = await request(app) + .post('/api/jobs') + .field('targetFormats', JSON.stringify(['gif'])) + .field('qualities', JSON.stringify([50])) + .attach('files', fixturePath, 'photo.png'); + + expect(response.status).toBe(201); + expect(response.body.jobs[0].error).toMatch(/Invalid quality/); + }); + + it('returns 400 when qualities length does not match the number of files', async () => { + const fixturePath = path.join(import.meta.dirname, '..', 'fixtures', 'sample.png'); + + const response = await request(app) + .post('/api/jobs') + .field('targetFormats', JSON.stringify(['jpg'])) + .field('qualities', JSON.stringify([10, 20])) + .attach('files', fixturePath, 'photo.png'); + + expect(response.status).toBe(400); }); it('rejects a file whose content does not match its extension, without failing the whole batch', async () => {