feat: add auto-increment id, size/duration tracking, and soft cleanup to conversion_jobs
Rename the old CHAR(36) id to uuid (still used for public URLs and file naming) and add a real auto-increment id as the primary key. Track input/output file size and conversion duration per job. Cleanup no longer deletes rows; it marks cleaned_at and skips already-cleaned expired jobs on later runs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+23
-15
@@ -6,7 +6,7 @@ import os from 'node:os';
|
||||
import { createApp } from '../../src/app.js';
|
||||
import { getPool, closePool } from '../../src/db.js';
|
||||
import { loadConfig } from '../../src/config.js';
|
||||
import { createJob, markDone } from '../../src/jobs/jobRepository.js';
|
||||
import { createJob, getJobByUuid, markDone } from '../../src/jobs/jobRepository.js';
|
||||
import { ensureStorageDirs, outputPath } from '../../src/storage.js';
|
||||
|
||||
let app;
|
||||
@@ -29,27 +29,29 @@ beforeEach(async () => {
|
||||
await pool.query('DELETE FROM conversion_jobs');
|
||||
});
|
||||
|
||||
function baseJob(id) {
|
||||
function baseJob(uuid) {
|
||||
return {
|
||||
id,
|
||||
uuid,
|
||||
family: 'image',
|
||||
sourceFormat: 'png',
|
||||
targetFormat: 'webp',
|
||||
originalFilename: 'holiday photo.png',
|
||||
inputPath: `${id}.png`,
|
||||
inputPath: `${uuid}.png`,
|
||||
inputMimeType: 'image/png',
|
||||
inputSizeBytes: 11,
|
||||
expiresAt: new Date(Date.now() + 3600 * 1000),
|
||||
};
|
||||
}
|
||||
|
||||
describe('GET /api/jobs/:id', () => {
|
||||
it('returns job status without the error log field', async () => {
|
||||
const id = '66666666-6666-4666-8666-666666666666';
|
||||
await createJob(pool, baseJob(id));
|
||||
const uuid = '66666666-6666-4666-8666-666666666666';
|
||||
await createJob(pool, baseJob(uuid));
|
||||
|
||||
const response = await request(app).get(`/api/jobs/${id}`);
|
||||
const response = await request(app).get(`/api/jobs/${uuid}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.id).toBe(uuid);
|
||||
expect(response.body.status).toBe('pending');
|
||||
expect(response.body.originalFilename).toBe('holiday photo.png');
|
||||
expect(response.body.errorLog).toBeUndefined();
|
||||
@@ -63,13 +65,19 @@ describe('GET /api/jobs/:id', () => {
|
||||
|
||||
describe('GET /api/jobs/:id/download', () => {
|
||||
it('streams the converted file with correct headers once done', async () => {
|
||||
const id = '77777777-7777-4777-8777-777777777777';
|
||||
await createJob(pool, baseJob(id));
|
||||
const filePath = outputPath(config, id, 'webp');
|
||||
const uuid = '77777777-7777-4777-8777-777777777777';
|
||||
await createJob(pool, baseJob(uuid));
|
||||
const filePath = outputPath(config, uuid, 'webp');
|
||||
await fs.writeFile(filePath, Buffer.from('fake webp bytes'));
|
||||
await markDone(pool, id, { outputPath: `${id}.webp`, outputMimeType: 'image/webp' });
|
||||
const created = await getJobByUuid(pool, uuid);
|
||||
await markDone(pool, created.id, {
|
||||
outputPath: `${uuid}.webp`,
|
||||
outputMimeType: 'image/webp',
|
||||
outputSizeBytes: 16,
|
||||
conversionDurationSeconds: 0.2,
|
||||
});
|
||||
|
||||
const response = await request(app).get(`/api/jobs/${id}/download`);
|
||||
const response = await request(app).get(`/api/jobs/${uuid}/download`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers['content-type']).toBe('image/webp');
|
||||
@@ -78,10 +86,10 @@ describe('GET /api/jobs/:id/download', () => {
|
||||
});
|
||||
|
||||
it('returns 409 when the job is not done yet', async () => {
|
||||
const id = '88888888-8888-4888-8888-888888888888';
|
||||
await createJob(pool, baseJob(id));
|
||||
const uuid = '88888888-8888-4888-8888-888888888888';
|
||||
await createJob(pool, baseJob(uuid));
|
||||
|
||||
const response = await request(app).get(`/api/jobs/${id}/download`);
|
||||
const response = await request(app).get(`/api/jobs/${uuid}/download`);
|
||||
|
||||
expect(response.status).toBe(409);
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import os from 'node:os';
|
||||
import { createApp } from '../../src/app.js';
|
||||
import { getPool, closePool } from '../../src/db.js';
|
||||
import { loadConfig } from '../../src/config.js';
|
||||
import { getJobById } from '../../src/jobs/jobRepository.js';
|
||||
import { getJobByUuid } from '../../src/jobs/jobRepository.js';
|
||||
import { ensureStorageDirs } from '../../src/storage.js';
|
||||
|
||||
let app;
|
||||
@@ -60,12 +60,13 @@ describe('POST /api/jobs', () => {
|
||||
expect(response.body.jobs[0].status).toBe('pending');
|
||||
expect(response.body.jobs[0].file).toBe('photo.png');
|
||||
|
||||
const job = await getJobById(pool, response.body.jobs[0].id);
|
||||
const job = await getJobByUuid(pool, response.body.jobs[0].id);
|
||||
expect(job.status).toBe('pending');
|
||||
expect(job.sourceFormat).toBe('png');
|
||||
expect(job.targetFormat).toBe('webp');
|
||||
expect(job.originalFilename).toBe('photo.png');
|
||||
expect(job.inputMimeType).toBe('image/png');
|
||||
expect(job.inputSizeBytes).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('rejects a file whose content does not match its extension, without failing the whole batch', async () => {
|
||||
|
||||
+63
-27
@@ -5,7 +5,7 @@ import os from 'node:os';
|
||||
import { getPool, closePool } from '../src/db.js';
|
||||
import { loadConfig } from '../src/config.js';
|
||||
import { ensureStorageDirs, uploadPath, outputPath } from '../src/storage.js';
|
||||
import { createJob, markDone, getJobById } from '../src/jobs/jobRepository.js';
|
||||
import { createJob, markDone, getJobByUuid } from '../src/jobs/jobRepository.js';
|
||||
import { runCleanup } from '../src/cleanup.js';
|
||||
|
||||
let pool;
|
||||
@@ -27,66 +27,102 @@ beforeEach(async () => {
|
||||
});
|
||||
|
||||
describe('runCleanup', () => {
|
||||
it('deletes an expired done job, its input file, and its output file', async () => {
|
||||
const id = 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee';
|
||||
await fs.writeFile(uploadPath(config, id, 'png'), 'input bytes');
|
||||
await fs.writeFile(outputPath(config, id, 'webp'), 'output bytes');
|
||||
it('marks an expired done job cleaned and removes its input and output files, keeping the row', async () => {
|
||||
const uuid = 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee';
|
||||
await fs.writeFile(uploadPath(config, uuid, 'png'), 'input bytes');
|
||||
await fs.writeFile(outputPath(config, uuid, 'webp'), 'output bytes');
|
||||
await createJob(pool, {
|
||||
id,
|
||||
uuid,
|
||||
family: 'image',
|
||||
sourceFormat: 'png',
|
||||
targetFormat: 'webp',
|
||||
originalFilename: 'photo.png',
|
||||
inputPath: `${id}.png`,
|
||||
inputPath: `${uuid}.png`,
|
||||
inputMimeType: 'image/png',
|
||||
inputSizeBytes: 11,
|
||||
expiresAt: new Date(Date.now() - 1000),
|
||||
});
|
||||
await markDone(pool, id, { outputPath: `${id}.webp`, outputMimeType: 'image/webp' });
|
||||
const created = await getJobByUuid(pool, uuid);
|
||||
await markDone(pool, created.id, {
|
||||
outputPath: `${uuid}.webp`,
|
||||
outputMimeType: 'image/webp',
|
||||
outputSizeBytes: 12,
|
||||
conversionDurationSeconds: 0.5,
|
||||
});
|
||||
|
||||
const deletedCount = await runCleanup(pool, config);
|
||||
const cleanedCount = await runCleanup(pool, config);
|
||||
|
||||
expect(deletedCount).toBe(1);
|
||||
expect(await getJobById(pool, id)).toBeNull();
|
||||
await expect(fs.stat(uploadPath(config, id, 'png'))).rejects.toThrow();
|
||||
await expect(fs.stat(outputPath(config, id, 'webp'))).rejects.toThrow();
|
||||
expect(cleanedCount).toBe(1);
|
||||
const job = await getJobByUuid(pool, uuid);
|
||||
expect(job).not.toBeNull();
|
||||
expect(job.cleanedAt).not.toBeNull();
|
||||
await expect(fs.stat(uploadPath(config, uuid, 'png'))).rejects.toThrow();
|
||||
await expect(fs.stat(outputPath(config, uuid, 'webp'))).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('deletes an expired pending job (with no output file) without throwing', async () => {
|
||||
const id = 'ffffffff-ffff-4fff-8fff-ffffffffffff';
|
||||
await fs.writeFile(uploadPath(config, id, 'png'), 'input bytes');
|
||||
it('marks an expired pending job (with no output file) cleaned without throwing', async () => {
|
||||
const uuid = 'ffffffff-ffff-4fff-8fff-ffffffffffff';
|
||||
await fs.writeFile(uploadPath(config, uuid, 'png'), 'input bytes');
|
||||
await createJob(pool, {
|
||||
id,
|
||||
uuid,
|
||||
family: 'image',
|
||||
sourceFormat: 'png',
|
||||
targetFormat: 'webp',
|
||||
originalFilename: 'photo.png',
|
||||
inputPath: `${id}.png`,
|
||||
inputPath: `${uuid}.png`,
|
||||
inputMimeType: 'image/png',
|
||||
inputSizeBytes: 11,
|
||||
expiresAt: new Date(Date.now() - 1000),
|
||||
});
|
||||
|
||||
const deletedCount = await runCleanup(pool, config);
|
||||
const cleanedCount = await runCleanup(pool, config);
|
||||
|
||||
expect(deletedCount).toBe(1);
|
||||
expect(await getJobById(pool, id)).toBeNull();
|
||||
expect(cleanedCount).toBe(1);
|
||||
const job = await getJobByUuid(pool, uuid);
|
||||
expect(job).not.toBeNull();
|
||||
expect(job.cleanedAt).not.toBeNull();
|
||||
});
|
||||
|
||||
it('leaves non-expired jobs untouched', async () => {
|
||||
const id = '12121212-1212-4212-8212-121212121212';
|
||||
const uuid = '12121212-1212-4212-8212-121212121212';
|
||||
await createJob(pool, {
|
||||
id,
|
||||
uuid,
|
||||
family: 'image',
|
||||
sourceFormat: 'png',
|
||||
targetFormat: 'webp',
|
||||
originalFilename: 'photo.png',
|
||||
inputPath: `${id}.png`,
|
||||
inputPath: `${uuid}.png`,
|
||||
inputMimeType: 'image/png',
|
||||
inputSizeBytes: 11,
|
||||
expiresAt: new Date(Date.now() + 3600 * 1000),
|
||||
});
|
||||
|
||||
const deletedCount = await runCleanup(pool, config);
|
||||
const cleanedCount = await runCleanup(pool, config);
|
||||
|
||||
expect(deletedCount).toBe(0);
|
||||
expect(await getJobById(pool, id)).not.toBeNull();
|
||||
expect(cleanedCount).toBe(0);
|
||||
const job = await getJobByUuid(pool, uuid);
|
||||
expect(job).not.toBeNull();
|
||||
expect(job.cleanedAt).toBeNull();
|
||||
});
|
||||
|
||||
it('does not re-process an already cleaned expired job', async () => {
|
||||
const uuid = '13131313-1313-4313-8313-131313131313';
|
||||
await createJob(pool, {
|
||||
uuid,
|
||||
family: 'image',
|
||||
sourceFormat: 'png',
|
||||
targetFormat: 'webp',
|
||||
originalFilename: 'photo.png',
|
||||
inputPath: `${uuid}.png`,
|
||||
inputMimeType: 'image/png',
|
||||
inputSizeBytes: 11,
|
||||
expiresAt: new Date(Date.now() - 1000),
|
||||
});
|
||||
|
||||
const firstRun = await runCleanup(pool, config);
|
||||
const secondRun = await runCleanup(pool, config);
|
||||
|
||||
expect(firstRun).toBe(1);
|
||||
expect(secondRun).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,14 +3,14 @@ import { getPool, closePool } from '../../src/db.js';
|
||||
import { loadConfig } from '../../src/config.js';
|
||||
import {
|
||||
createJob,
|
||||
getJobById,
|
||||
getJobByUuid,
|
||||
getJobErrorLog,
|
||||
markProcessing,
|
||||
markDone,
|
||||
markFailed,
|
||||
findPendingJobs,
|
||||
findExpiredJobs,
|
||||
deleteJob,
|
||||
markCleaned,
|
||||
} from '../../src/jobs/jobRepository.js';
|
||||
|
||||
let pool;
|
||||
@@ -29,13 +29,14 @@ beforeEach(async () => {
|
||||
|
||||
function baseJob(overrides = {}) {
|
||||
return {
|
||||
id: overrides.id ?? '11111111-1111-4111-8111-111111111111',
|
||||
uuid: overrides.uuid ?? '11111111-1111-4111-8111-111111111111',
|
||||
family: 'image',
|
||||
sourceFormat: 'png',
|
||||
targetFormat: 'webp',
|
||||
originalFilename: 'photo.png',
|
||||
inputPath: `${overrides.id ?? '11111111-1111-4111-8111-111111111111'}.png`,
|
||||
inputPath: `${overrides.uuid ?? '11111111-1111-4111-8111-111111111111'}.png`,
|
||||
inputMimeType: 'image/png',
|
||||
inputSizeBytes: 1024,
|
||||
expiresAt: new Date(Date.now() + 3600 * 1000),
|
||||
...overrides,
|
||||
};
|
||||
@@ -45,78 +46,94 @@ describe('jobRepository', () => {
|
||||
it('creates and retrieves a pending job', async () => {
|
||||
await createJob(pool, baseJob());
|
||||
|
||||
const job = await getJobById(pool, '11111111-1111-4111-8111-111111111111');
|
||||
const job = await getJobByUuid(pool, '11111111-1111-4111-8111-111111111111');
|
||||
|
||||
expect(job.id).toEqual(expect.any(Number));
|
||||
expect(job.uuid).toBe('11111111-1111-4111-8111-111111111111');
|
||||
expect(job.status).toBe('pending');
|
||||
expect(job.family).toBe('image');
|
||||
expect(job.sourceFormat).toBe('png');
|
||||
expect(job.targetFormat).toBe('webp');
|
||||
expect(job.originalFilename).toBe('photo.png');
|
||||
expect(job.inputMimeType).toBe('image/png');
|
||||
expect(job.inputSizeBytes).toBe(1024);
|
||||
expect(job.outputPath).toBeNull();
|
||||
expect(job.outputSizeBytes).toBeNull();
|
||||
expect(job.conversionDurationSeconds).toBeNull();
|
||||
expect(job.errorMessage).toBeNull();
|
||||
expect(job.cleanedAt).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for an unknown id', async () => {
|
||||
const job = await getJobById(pool, '22222222-2222-4222-8222-222222222222');
|
||||
it('returns null for an unknown uuid', async () => {
|
||||
const job = await getJobByUuid(pool, '22222222-2222-4222-8222-222222222222');
|
||||
expect(job).toBeNull();
|
||||
});
|
||||
|
||||
it('transitions a job through processing to done', async () => {
|
||||
await createJob(pool, baseJob());
|
||||
|
||||
await markProcessing(pool, '11111111-1111-4111-8111-111111111111');
|
||||
const processing = await getJobById(pool, '11111111-1111-4111-8111-111111111111');
|
||||
await markProcessing(pool, (await getJobByUuid(pool, '11111111-1111-4111-8111-111111111111')).id);
|
||||
const processing = await getJobByUuid(pool, '11111111-1111-4111-8111-111111111111');
|
||||
expect(processing.status).toBe('processing');
|
||||
|
||||
await markDone(pool, '11111111-1111-4111-8111-111111111111', {
|
||||
await markDone(pool, processing.id, {
|
||||
outputPath: '11111111-1111-4111-8111-111111111111.webp',
|
||||
outputMimeType: 'image/webp',
|
||||
outputSizeBytes: 2048,
|
||||
conversionDurationSeconds: 1.5,
|
||||
});
|
||||
const done = await getJobById(pool, '11111111-1111-4111-8111-111111111111');
|
||||
const done = await getJobByUuid(pool, '11111111-1111-4111-8111-111111111111');
|
||||
expect(done.status).toBe('done');
|
||||
expect(done.outputPath).toBe('11111111-1111-4111-8111-111111111111.webp');
|
||||
expect(done.outputMimeType).toBe('image/webp');
|
||||
expect(done.outputSizeBytes).toBe(2048);
|
||||
expect(Number(done.conversionDurationSeconds)).toBe(1.5);
|
||||
});
|
||||
|
||||
it('marks a job failed with a short message and a separate detailed log', async () => {
|
||||
await createJob(pool, baseJob());
|
||||
const created = await getJobByUuid(pool, '11111111-1111-4111-8111-111111111111');
|
||||
|
||||
await markFailed(pool, '11111111-1111-4111-8111-111111111111', {
|
||||
await markFailed(pool, created.id, {
|
||||
errorMessage: 'Conversion failed, please try again',
|
||||
errorLog: 'Error: sharp threw at line 42\n at convert (image.js:10:5)',
|
||||
});
|
||||
|
||||
const job = await getJobById(pool, '11111111-1111-4111-8111-111111111111');
|
||||
const job = await getJobByUuid(pool, '11111111-1111-4111-8111-111111111111');
|
||||
expect(job.status).toBe('failed');
|
||||
expect(job.errorMessage).toBe('Conversion failed, please try again');
|
||||
expect(job.errorLog).toBeUndefined();
|
||||
|
||||
const errorLog = await getJobErrorLog(pool, '11111111-1111-4111-8111-111111111111');
|
||||
const errorLog = await getJobErrorLog(pool, created.id);
|
||||
expect(errorLog).toBe('Error: sharp threw at line 42\n at convert (image.js:10:5)');
|
||||
});
|
||||
|
||||
it('finds pending jobs oldest first, up to a limit', async () => {
|
||||
await createJob(pool, baseJob({ id: '33333333-3333-4333-8333-333333333333' }));
|
||||
await createJob(pool, baseJob({ id: '44444444-4444-4444-8444-444444444444' }));
|
||||
await createJob(pool, baseJob({ id: '55555555-5555-4555-8555-555555555555' }));
|
||||
await createJob(pool, baseJob({ uuid: '33333333-3333-4333-8333-333333333333' }));
|
||||
await createJob(pool, baseJob({ uuid: '44444444-4444-4444-8444-444444444444' }));
|
||||
await createJob(pool, baseJob({ uuid: '55555555-5555-4555-8555-555555555555' }));
|
||||
|
||||
const jobs = await findPendingJobs(pool, 2);
|
||||
|
||||
expect(jobs).toHaveLength(2);
|
||||
expect(jobs[0].id).toBe('33333333-3333-4333-8333-333333333333');
|
||||
expect(jobs[1].id).toBe('44444444-4444-4444-8444-444444444444');
|
||||
expect(jobs[0].uuid).toBe('33333333-3333-4333-8333-333333333333');
|
||||
expect(jobs[1].uuid).toBe('44444444-4444-4444-8444-444444444444');
|
||||
});
|
||||
|
||||
it('finds expired jobs and allows deleting them', async () => {
|
||||
it('finds expired jobs and allows marking them cleaned without deleting the row', async () => {
|
||||
await createJob(pool, baseJob({ expiresAt: new Date(Date.now() - 1000) }));
|
||||
|
||||
const expired = await findExpiredJobs(pool);
|
||||
expect(expired).toHaveLength(1);
|
||||
expect(expired[0].id).toBe('11111111-1111-4111-8111-111111111111');
|
||||
expect(expired[0].uuid).toBe('11111111-1111-4111-8111-111111111111');
|
||||
|
||||
await deleteJob(pool, '11111111-1111-4111-8111-111111111111');
|
||||
const afterDelete = await getJobById(pool, '11111111-1111-4111-8111-111111111111');
|
||||
expect(afterDelete).toBeNull();
|
||||
await markCleaned(pool, expired[0].id);
|
||||
|
||||
const stillPresent = await getJobByUuid(pool, '11111111-1111-4111-8111-111111111111');
|
||||
expect(stillPresent).not.toBeNull();
|
||||
expect(stillPresent.cleanedAt).not.toBeNull();
|
||||
|
||||
const afterCleaning = await findExpiredJobs(pool);
|
||||
expect(afterCleaning).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
+20
-15
@@ -5,7 +5,7 @@ import os from 'node:os';
|
||||
import { getPool, closePool } from '../src/db.js';
|
||||
import { loadConfig } from '../src/config.js';
|
||||
import { ensureStorageDirs, uploadPath, outputPath } from '../src/storage.js';
|
||||
import { createJob, getJobById, getJobErrorLog } from '../src/jobs/jobRepository.js';
|
||||
import { createJob, getJobByUuid, getJobErrorLog } from '../src/jobs/jobRepository.js';
|
||||
import { registerImageConverters } from '../src/converters/image.js';
|
||||
import { processPendingJobs } from '../src/worker.js';
|
||||
|
||||
@@ -28,61 +28,66 @@ beforeEach(async () => {
|
||||
await pool.query('DELETE FROM conversion_jobs');
|
||||
});
|
||||
|
||||
async function createPendingImageJob(id, sourceFormat, targetFormat) {
|
||||
async function createPendingImageJob(uuid, sourceFormat, targetFormat) {
|
||||
const fixturePath = path.join(import.meta.dirname, 'fixtures', 'sample.png');
|
||||
const inputFilePath = uploadPath(config, id, sourceFormat);
|
||||
const inputFilePath = uploadPath(config, uuid, sourceFormat);
|
||||
await fs.copyFile(fixturePath, inputFilePath);
|
||||
const { size: inputSizeBytes } = await fs.stat(inputFilePath);
|
||||
|
||||
await createJob(pool, {
|
||||
id,
|
||||
uuid,
|
||||
family: 'image',
|
||||
sourceFormat,
|
||||
targetFormat,
|
||||
originalFilename: `photo.${sourceFormat}`,
|
||||
inputPath: `${id}.${sourceFormat}`,
|
||||
inputPath: `${uuid}.${sourceFormat}`,
|
||||
inputMimeType: 'image/png',
|
||||
inputSizeBytes,
|
||||
expiresAt: new Date(Date.now() + 3600 * 1000),
|
||||
});
|
||||
}
|
||||
|
||||
describe('processPendingJobs', () => {
|
||||
it('converts a pending image job to done', async () => {
|
||||
const id = '99999999-9999-4999-8999-999999999999';
|
||||
await createPendingImageJob(id, 'png', 'webp');
|
||||
const uuid = '99999999-9999-4999-8999-999999999999';
|
||||
await createPendingImageJob(uuid, 'png', 'webp');
|
||||
|
||||
const processedCount = await processPendingJobs(pool, config);
|
||||
|
||||
expect(processedCount).toBe(1);
|
||||
const job = await getJobById(pool, id);
|
||||
const job = await getJobByUuid(pool, uuid);
|
||||
expect(job.status).toBe('done');
|
||||
expect(job.outputPath).toBe(`${id}.webp`);
|
||||
expect(job.outputPath).toBe(`${uuid}.webp`);
|
||||
expect(job.outputMimeType).toBe('image/webp');
|
||||
expect(job.outputSizeBytes).toBeGreaterThan(0);
|
||||
expect(Number(job.conversionDurationSeconds)).toBeGreaterThanOrEqual(0);
|
||||
|
||||
const stat = await fs.stat(outputPath(config, id, 'webp'));
|
||||
const stat = await fs.stat(outputPath(config, uuid, 'webp'));
|
||||
expect(stat.size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('marks a job failed with a safe message and a detailed log when the converter throws', async () => {
|
||||
const id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
|
||||
const uuid = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
|
||||
await createJob(pool, {
|
||||
id,
|
||||
uuid,
|
||||
family: 'image',
|
||||
sourceFormat: 'png',
|
||||
targetFormat: 'webp',
|
||||
originalFilename: 'missing.png',
|
||||
inputPath: `${id}.png`,
|
||||
inputPath: `${uuid}.png`,
|
||||
inputMimeType: 'image/png',
|
||||
inputSizeBytes: 11,
|
||||
expiresAt: new Date(Date.now() + 3600 * 1000),
|
||||
});
|
||||
// Note: input file is intentionally never written, so sharp will throw ENOENT.
|
||||
|
||||
await processPendingJobs(pool, config);
|
||||
|
||||
const job = await getJobById(pool, id);
|
||||
const job = await getJobByUuid(pool, uuid);
|
||||
expect(job.status).toBe('failed');
|
||||
expect(job.errorMessage).toBe('Conversion failed, please try again.');
|
||||
|
||||
const errorLog = await getJobErrorLog(pool, id);
|
||||
const errorLog = await getJobErrorLog(pool, job.id);
|
||||
expect(errorLog).toMatch(/input file is missing/i);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user