import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import { getPrismaClient, closePrismaClient } from '../../src/db.js'; import { loadConfig } from '../../src/config.js'; import { createJob, getJobByUuid, getJobErrorLog, markProcessing, markDone, markFailed, findPendingJobs, findExpiredJobs, markCleaned, } from '../../src/jobs/jobRepository.js'; let prisma; beforeAll(() => { prisma = getPrismaClient(loadConfig()); }); afterAll(async () => { await closePrismaClient(); }); beforeEach(async () => { await prisma.conversionJob.deleteMany(); }); function baseJob(overrides = {}) { return { uuid: overrides.uuid ?? '11111111-1111-4111-8111-111111111111', family: 'image', sourceFormat: 'png', targetFormat: 'webp', originalFilename: 'photo.png', inputPath: `${overrides.uuid ?? '11111111-1111-4111-8111-111111111111'}.png`, inputMimeType: 'image/png', inputSizeBytes: 1024, expiresAt: new Date(Date.now() + 3600 * 1000), ...overrides, }; } describe('jobRepository', () => { it('creates and retrieves a pending job', async () => { await createJob(prisma, baseJob()); const job = await getJobByUuid(prisma, '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(); expect(job.quality).toBeNull(); expect(job.iconSize).toBeNull(); }); it('stores and retrieves a numeric quality value', async () => { await createJob(prisma, baseJob({ uuid: '66666666-6666-4666-8666-666666666666', quality: 82 })); const job = await getJobByUuid(prisma, '66666666-6666-4666-8666-666666666666'); expect(job.quality).toBe(82); }); it('stores and retrieves a numeric iconSize value', async () => { await createJob( prisma, baseJob({ uuid: '77777777-7777-4777-8777-777777777777', targetFormat: 'ico', iconSize: 48 }) ); const job = await getJobByUuid(prisma, '77777777-7777-4777-8777-777777777777'); expect(job.iconSize).toBe(48); }); it('returns null for an unknown uuid', async () => { const job = await getJobByUuid(prisma, '22222222-2222-4222-8222-222222222222'); expect(job).toBeNull(); }); it('transitions a job through processing to done', async () => { await createJob(prisma, baseJob()); await markProcessing(prisma, (await getJobByUuid(prisma, '11111111-1111-4111-8111-111111111111')).id); const processing = await getJobByUuid(prisma, '11111111-1111-4111-8111-111111111111'); expect(processing.status).toBe('processing'); await markDone(prisma, processing.id, { outputPath: '11111111-1111-4111-8111-111111111111.webp', outputMimeType: 'image/webp', outputSizeBytes: 2048, conversionDurationSeconds: 1.5, }); const done = await getJobByUuid(prisma, '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(prisma, baseJob()); const created = await getJobByUuid(prisma, '11111111-1111-4111-8111-111111111111'); await markFailed(prisma, 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 getJobByUuid(prisma, '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(prisma, 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(prisma, baseJob({ uuid: '33333333-3333-4333-8333-333333333333' })); await createJob(prisma, baseJob({ uuid: '44444444-4444-4444-8444-444444444444' })); await createJob(prisma, baseJob({ uuid: '55555555-5555-4555-8555-555555555555' })); const jobs = await findPendingJobs(prisma, 2); expect(jobs).toHaveLength(2); 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 marking them cleaned without deleting the row', async () => { await createJob(prisma, baseJob({ expiresAt: new Date(Date.now() - 1000) })); const expired = await findExpiredJobs(prisma); expect(expired).toHaveLength(1); expect(expired[0].uuid).toBe('11111111-1111-4111-8111-111111111111'); await markCleaned(prisma, expired[0].id); const stillPresent = await getJobByUuid(prisma, '11111111-1111-4111-8111-111111111111'); expect(stillPresent).not.toBeNull(); expect(stillPresent.cleanedAt).not.toBeNull(); const afterCleaning = await findExpiredJobs(prisma); expect(afterCleaning).toHaveLength(0); }); });