import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import request from 'supertest'; import fs from 'node:fs/promises'; import path from 'node:path'; 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 { getJobByUuid } from '../../src/jobs/jobRepository.js'; import { ensureStorageDirs } from '../../src/storage.js'; let app; let pool; let config; beforeAll(async () => { config = { ...loadConfig(), storageDir: await fs.mkdtemp(path.join(os.tmpdir(), 'converter-api-')) }; await ensureStorageDirs(config); pool = getPool(config); app = createApp(config, pool); }); afterAll(async () => { await closePool(); await fs.rm(config.storageDir, { recursive: true, force: true }); }); beforeEach(async () => { await pool.query('DELETE FROM conversion_jobs'); }); describe('GET /api/formats', () => { it('lists valid target formats for a known source format', async () => { const response = await request(app).get('/api/formats').query({ source: 'png' }); expect(response.status).toBe(200); expect(response.body.targets).toContain('webp'); expect(response.body.targets).toContain('pdf'); }); it('returns an empty list for an unknown source format', async () => { const response = await request(app).get('/api/formats').query({ source: 'made-up' }); expect(response.status).toBe(200); expect(response.body.targets).toEqual([]); }); }); describe('POST /api/jobs', () => { it('creates a pending job for a valid image upload', async () => { const fixturePath = path.join(import.meta.dirname, '..', 'fixtures', 'sample.png'); const response = await request(app) .post('/api/jobs') .field('targetFormats', JSON.stringify(['webp'])) .attach('files', fixturePath, 'photo.png'); expect(response.status).toBe(201); expect(response.body.jobs).toHaveLength(1); expect(response.body.jobs[0].status).toBe('pending'); expect(response.body.jobs[0].file).toBe('photo.png'); 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 () => { const fakePath = path.join(config.storageDir, 'fake.png'); await fs.writeFile(fakePath, 'not actually a png'); const response = await request(app) .post('/api/jobs') .field('targetFormats', JSON.stringify(['webp'])) .attach('files', fakePath, 'fake.png'); expect(response.status).toBe(201); expect(response.body.jobs[0].error).toMatch(/does not match/); await fs.unlink(fakePath); }); it('rejects an unsupported source/target pair', async () => { const fixturePath = path.join(import.meta.dirname, '..', 'fixtures', 'sample.png'); const response = await request(app) .post('/api/jobs') .field('targetFormats', JSON.stringify(['made-up-format'])) .attach('files', fixturePath, 'photo.png'); expect(response.status).toBe(201); expect(response.body.jobs[0].error).toMatch(/Unsupported conversion/); }); it('returns 400 when targetFormats 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([])) .attach('files', fixturePath, 'photo.png'); expect(response.status).toBe(400); }); it('returns 400 when no files are uploaded', async () => { const response = await request(app) .post('/api/jobs') .field('targetFormats', JSON.stringify([])); expect(response.status).toBe(400); }); });