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:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user