Raise Jest coverage above global thresholds.

Add real MCP HTTP/tool/entrypoint tests and extend torrent/qbt coverage
so statements, branches, functions, and lines all meet the configured floors.
This commit is contained in:
2026-08-07 19:37:22 +02:00
parent ac97862edb
commit b04e70e261
5 changed files with 549 additions and 7 deletions

View File

@@ -663,3 +663,219 @@ describe('download pipeline', () => {
expect(body[0].name).toBe('renamed.mkv');
});
});
// ── exported helpers + extra routes ──────────────────────────────────────────
describe('getQueue / enqueueDirect / files / re-hash', () => {
beforeEach(() => {
cleanState();
process.env.MAX_CONCURRENT_DOWNLOADS = '0';
});
it('getQueue returns queue snapshot', async () => {
const { app, qbt } = makeApp();
const torrent = makeTorrentBuf('http://webshare-api:3001/stream/gq1');
await request(app).post('/api/v2/torrents/add').attach('torrents', torrent, 'gq.torrent');
const q = qbt.getQueue();
expect(q).toHaveLength(1);
expect(q[0]).toMatchObject({ name: 'gq', state: 'queuedDL' });
expect(q[0].path).toContain(tmpDir);
});
it('enqueueDirect stages a webshare download and rejects duplicates', () => {
process.env.BASE_URL = 'http://localhost:3001';
const { qbt } = makeApp();
expect(qbt.enqueueDirect('ident99', 'Show.S01E01.mkv', 1000)).toBe(true);
expect(qbt.enqueueDirect('ident99', 'Show.S01E01.mkv', 1000)).toBe(false);
const q = qbt.getQueue();
expect(q).toHaveLength(1);
expect(q[0].name).toBe('Show.S01E01.mkv');
expect(q[0].path).toContain('Show.S01E01.mkv');
});
it('GET /api/v2/torrents/files returns file list for known hash', async () => {
const { app } = makeApp();
const torrent = makeTorrentBuf('http://webshare-api:3001/stream/files1');
await request(app).post('/api/v2/torrents/add').attach('torrents', torrent, 'files.torrent');
const { body: items } = await request(app).get('/api/v2/torrents/info');
const hash = items[0].hash;
const res = await request(app).get(`/api/v2/torrents/files?hash=${hash}`);
expect(res.status).toBe(200);
expect(res.body[0].name).toBe('files');
expect(res.body[0].priority).toBe(1);
const missing = await request(app).get('/api/v2/torrents/files?hash=nope');
expect(missing.status).toBe(404);
});
it('re-maps existing entry when same webseed is re-added with different hash', async () => {
const { app } = makeApp();
const webseed = 'http://webshare-api:3001/stream/rehash1';
const t1 = makeTorrentBuf(webseed);
await request(app).post('/api/v2/torrents/add').attach('torrents', t1, 'old.torrent');
// Manually corrupt hash in state by adding via url first then torrent — add via url uses sha1(url)
await request(app).post('/api/v2/torrents/add').send(`urls=${webseed}`);
// second torrent add with same webseed should re-hash path, not create second download
await request(app).post('/api/v2/torrents/add').attach('torrents', t1, 'new.torrent');
const { body } = await request(app).get('/api/v2/torrents/info');
// one logical download for the webseed
const sameUrl = body.filter((b) => b.name === 'old' || b.name === 'new' || b.name === 'rehash1');
expect(sameUrl.length).toBeGreaterThanOrEqual(1);
// overall should not keep exploding
expect(body.length).toBeLessThanOrEqual(2);
});
it('cancelDownload returns false for unknown hash and true for queued', async () => {
const { app, qbt } = makeApp();
expect(qbt.cancelDownload('missing')).toBe(false);
expect(qbt.cancelDownload('')).toBe(false);
const torrent = makeTorrentBuf('http://webshare-api:3001/stream/canc1');
await request(app).post('/api/v2/torrents/add').attach('torrents', torrent, 'c.torrent');
const { body } = await request(app).get('/api/v2/torrents/info');
expect(qbt.cancelDownload(body[0].hash)).toBe(true);
expect((await request(app).get('/api/v2/torrents/info')).body).toHaveLength(0);
});
it('safeFileName strips path and unsafe characters via enqueueDirect', () => {
const { qbt } = makeApp();
qbt.enqueueDirect('x', '../../evil:name?.mkv', 1);
expect(qbt.getQueue()[0].name).toBe('evil_name_.mkv');
});
it('re-hashes entry when torrent re-add has same webseed but different info-hash name', async () => {
const { app } = makeApp();
const webseed = 'http://webshare-api:3001/stream/samews';
// First: add via URL (hash = sha1(url), name = ident)
await request(app).post('/api/v2/torrents/add').send(`urls=${webseed}`);
let { body } = await request(app).get('/api/v2/torrents/info');
expect(body).toHaveLength(1);
const oldHash = body[0].hash;
// Second: same webseed via .torrent → extractInfoHash differs from sha1(url)
const torrent = makeTorrentBuf(webseed);
await request(app).post('/api/v2/torrents/add').attach('torrents', torrent, 'samews.torrent');
({ body } = await request(app).get('/api/v2/torrents/info'));
expect(body).toHaveLength(1);
expect(body[0].hash).not.toBe(oldHash);
});
it('cancelDownload aborts active HTTP request', async () => {
process.env.MAX_CONCURRENT_DOWNLOADS = '1';
const { EventEmitter } = require('events');
const httpMod = require('http');
const reqObj = { destroy: jest.fn(), on: jest.fn() };
jest.spyOn(httpMod, 'get').mockImplementation((url, cb) => {
const res = new EventEmitter();
res.statusCode = 200;
res.headers = { 'content-length': '1000000' };
res.destroy = jest.fn();
res.resume = jest.fn();
res.pipe = jest.fn(() => res);
// never finish — stays "downloading"
setImmediate(() => cb(res));
return reqObj;
});
jest.spyOn(fs, 'createWriteStream').mockReturnValue(
(() => {
const w = new EventEmitter();
w.close = (cb) => cb && cb();
return w;
})()
);
const { app, qbt } = makeApp();
const torrent = makeTorrentBuf('http://webshare-api:3001/stream/actcancel');
await request(app).post('/api/v2/torrents/add').attach('torrents', torrent, 'ac.torrent');
await new Promise((r) => setTimeout(r, 50));
const { body } = await request(app).get('/api/v2/torrents/info');
expect(body[0].state).toBe('downloading');
expect(qbt.cancelDownload(body[0].hash)).toBe(true);
expect(reqObj.destroy).toHaveBeenCalled();
process.env.MAX_CONCURRENT_DOWNLOADS = '0';
jest.restoreAllMocks();
});
it('follows 303 redirect then completes', async () => {
process.env.MAX_CONCURRENT_DOWNLOADS = '1';
const { EventEmitter } = require('events');
const httpMod = require('http');
let call = 0;
jest.spyOn(httpMod, 'get').mockImplementation((url, cb) => {
call++;
const res = new EventEmitter();
if (call === 1) {
res.statusCode = 303;
res.headers = { location: 'http://cdn.example.com/f.mkv' };
res.resume = jest.fn();
setImmediate(() => cb(res));
} else {
res.statusCode = 200;
res.headers = { 'content-length': '4' };
res.destroy = jest.fn();
res.pipe = jest.fn((dest) => {
setImmediate(() => dest.emit('finish'));
return dest;
});
setImmediate(() => cb(res));
}
return { on: jest.fn() };
});
jest.spyOn(fs, 'createWriteStream').mockReturnValue(
(() => {
const w = new EventEmitter();
w.close = (cb) => cb && cb();
return w;
})()
);
const { app } = makeApp();
await request(app)
.post('/api/v2/torrents/add')
.attach('torrents', makeTorrentBuf('http://webshare-api:3001/stream/r303'), 'r.torrent');
await new Promise((r) => setTimeout(r, 120));
expect(call).toBe(2);
process.env.MAX_CONCURRENT_DOWNLOADS = '0';
jest.restoreAllMocks();
});
it('updates speed on data chunks during download', async () => {
process.env.MAX_CONCURRENT_DOWNLOADS = '1';
const { EventEmitter } = require('events');
const httpMod = require('http');
jest.spyOn(httpMod, 'get').mockImplementation((url, cb) => {
const res = new EventEmitter();
res.statusCode = 200;
res.headers = { 'content-length': '10000' };
res.destroy = jest.fn();
res.resume = jest.fn();
res.pipe = jest.fn((dest) => {
setImmediate(() => {
res.emit('data', Buffer.alloc(3000));
res.emit('data', Buffer.alloc(3000));
dest.emit('finish');
});
return dest;
});
setImmediate(() => cb(res));
return { on: jest.fn() };
});
jest.spyOn(fs, 'createWriteStream').mockReturnValue(
(() => {
const w = new EventEmitter();
w.close = (cb) => cb && cb();
return w;
})()
);
const { app } = makeApp();
const torrent = makeTorrentBuf('http://webshare-api:3001/stream/speed1');
await request(app).post('/api/v2/torrents/add').attach('torrents', torrent, 'sp.torrent');
await new Promise((r) => setTimeout(r, 150));
const { body } = await request(app).get('/api/v2/torrents/info');
expect(body[0].state).toBe('uploading');
process.env.MAX_CONCURRENT_DOWNLOADS = '0';
jest.restoreAllMocks();
});
});