feat(archive): wire archive converters, validation, and extension parsing into app.js/worker.js

Also fixes a third double-extension bug the plan missed: app.js derived
each job's uuid via path.basename(file.filename, path.extname(...)),
which only strips the last dot segment. For a compound extension like
tar.gz this left "<uuid>.tar" as the "uuid", overflowing the uuid
column (Char(36)) and crashing the request. Replaced with
stripExtension(). Also bumped rateLimitMaxJobs in jobs.test.js's config
override — the file's cumulative POST /api/jobs calls across all tests
now exceeds the production default (20/window) once the archive tests
are included, causing spurious connection resets independent of any
real bug.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 13:00:01 +02:00
co-authored by Claude Sonnet 5
parent a8cdb06d5c
commit ef6de6b583
3 changed files with 114 additions and 6 deletions
+103 -1
View File
@@ -8,13 +8,20 @@ import { getPrismaClient, closePrismaClient } from '../../src/db.js';
import { loadConfig } from '../../src/config.js';
import { getJobByUuid } from '../../src/jobs/jobRepository.js';
import { ensureStorageDirs } from '../../src/storage.js';
import AdmZip from 'adm-zip';
let app;
let prisma;
let config;
beforeAll(async () => {
config = { ...loadConfig(), storageDir: await fs.mkdtemp(path.join(os.tmpdir(), 'converter-api-')) };
config = {
...loadConfig(),
storageDir: await fs.mkdtemp(path.join(os.tmpdir(), 'converter-api-')),
// This file's cumulative POST /api/jobs calls across all tests exceeds the
// production-tuned default (20/window) once the archive tests are included.
rateLimitMaxJobs: 1000,
};
await ensureStorageDirs(config);
prisma = getPrismaClient(config);
app = createApp(config, prisma);
@@ -343,3 +350,98 @@ describe('POST /api/jobs', () => {
expect(response.status).toBe(400);
});
});
function buildZipFixture(destPath) {
const zip = new AdmZip();
zip.addFile('hello.txt', Buffer.from('hello world'));
zip.writeZip(destPath);
}
describe('POST /api/jobs — archives', () => {
it('creates a pending job converting a zip upload to tar.gz', async () => {
const fixturePath = path.join(config.storageDir, 'archive.zip');
buildZipFixture(fixturePath);
const response = await request(app)
.post('/api/jobs')
.field('targetFormats', JSON.stringify(['tar.gz']))
.attach('files', fixturePath, 'archive.zip');
expect(response.status).toBe(201);
expect(response.body.jobs[0].status).toBe('pending');
const job = await getJobByUuid(prisma, response.body.jobs[0].id);
expect(job.sourceFormat).toBe('zip');
expect(job.targetFormat).toBe('tar.gz');
expect(job.family).toBe('archive');
await fs.unlink(fixturePath);
});
it('creates a pending job with a compression level for a same-format zip -> zip conversion', async () => {
const fixturePath = path.join(config.storageDir, 'recompress.zip');
buildZipFixture(fixturePath);
const response = await request(app)
.post('/api/jobs')
.field('targetFormats', JSON.stringify(['zip']))
.field('qualities', JSON.stringify([9]))
.attach('files', fixturePath, 'recompress.zip');
expect(response.status).toBe(201);
const job = await getJobByUuid(prisma, response.body.jobs[0].id);
expect(job.targetFormat).toBe('zip');
expect(job.quality).toBe(9);
await fs.unlink(fixturePath);
});
it('rejects an out-of-range compression level for a zip target', async () => {
const fixturePath = path.join(config.storageDir, 'bad-level.zip');
buildZipFixture(fixturePath);
const response = await request(app)
.post('/api/jobs')
.field('targetFormats', JSON.stringify(['zip']))
.field('qualities', JSON.stringify([15]))
.attach('files', fixturePath, 'bad-level.zip');
expect(response.status).toBe(201);
expect(response.body.jobs[0].error).toMatch(/Invalid quality/);
await fs.unlink(fixturePath);
});
it('rejects a compression level for a tar target (no compression to control)', async () => {
const fixturePath = path.join(config.storageDir, 'for-tar.zip');
buildZipFixture(fixturePath);
const response = await request(app)
.post('/api/jobs')
.field('targetFormats', JSON.stringify(['tar']))
.field('qualities', JSON.stringify([5]))
.attach('files', fixturePath, 'for-tar.zip');
expect(response.status).toBe(201);
expect(response.body.jobs[0].error).toMatch(/Invalid quality/);
await fs.unlink(fixturePath);
});
it('parses a compound .tar.gz original filename as a single source format, not "gz"', async () => {
const zlib = await import('node:zlib');
const fixturePath = path.join(config.storageDir, 'backup.tar.gz');
await fs.writeFile(fixturePath, zlib.gzipSync(Buffer.from('irrelevant payload for this check')));
const response = await request(app)
.post('/api/jobs')
.field('targetFormats', JSON.stringify(['zip']))
.attach('files', fixturePath, 'backup.tar.gz');
expect(response.status).toBe(201);
const job = await getJobByUuid(prisma, response.body.jobs[0].id);
expect(job.sourceFormat).toBe('tar.gz');
await fs.unlink(fixturePath);
});
});