145 lines
6.3 KiB
TypeScript
145 lines
6.3 KiB
TypeScript
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import { DelugeClient, DelugeConfigFlow, DelugeIntegration, DelugeMapper, type IDelugeRawData } from '../../ts/integrations/deluge/index.js';
|
|
|
|
interface IDelugeWebTestServer {
|
|
url: string;
|
|
calls: Array<{ method: string; params: unknown[] }>;
|
|
close(): Promise<void>;
|
|
}
|
|
|
|
const readBody = async (requestArg: IncomingMessage): Promise<Record<string, unknown>> => {
|
|
const chunks: Buffer[] = [];
|
|
for await (const chunk of requestArg) {
|
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
}
|
|
return JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record<string, unknown>;
|
|
};
|
|
|
|
const jsonRpc = (responseArg: ServerResponse, idArg: unknown, resultArg: unknown, errorArg: unknown = null): void => {
|
|
responseArg.statusCode = 200;
|
|
responseArg.setHeader('content-type', 'application/json');
|
|
responseArg.end(JSON.stringify({ id: idArg, result: resultArg, error: errorArg }));
|
|
};
|
|
|
|
const startDelugeWebServer = async (): Promise<IDelugeWebTestServer> => {
|
|
const calls: Array<{ method: string; params: unknown[] }> = [];
|
|
const server = createServer(async (requestArg: IncomingMessage, responseArg: ServerResponse) => {
|
|
if (requestArg.method !== 'POST' || requestArg.url !== '/json') {
|
|
responseArg.statusCode = 404;
|
|
responseArg.end('not found');
|
|
return;
|
|
}
|
|
const body = await readBody(requestArg);
|
|
const method = String(body.method);
|
|
const params = Array.isArray(body.params) ? body.params : [];
|
|
calls.push({ method, params });
|
|
|
|
if (method === 'auth.login') {
|
|
const ok = params[0] === 'deluge';
|
|
if (ok) {
|
|
responseArg.setHeader('set-cookie', '_session_id=testsession; Path=/');
|
|
}
|
|
jsonRpc(responseArg, body.id, ok);
|
|
return;
|
|
}
|
|
if (!requestArg.headers.cookie?.includes('_session_id=testsession')) {
|
|
jsonRpc(responseArg, body.id, null, { message: 'Not authenticated', code: 1 });
|
|
return;
|
|
}
|
|
if (method === 'web.connected') {
|
|
jsonRpc(responseArg, body.id, true);
|
|
return;
|
|
}
|
|
if (method === 'web.update_ui') {
|
|
jsonRpc(responseArg, body.id, {
|
|
connected: true,
|
|
stats: { download_rate: 4096, upload_rate: 1024, download_protocol_rate: 512, upload_protocol_rate: 256 },
|
|
torrents: {
|
|
abc: { name: 'Ubuntu ISO', state: 'Downloading', paused: false, progress: 50 },
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
if (method === 'core.get_session_status') {
|
|
jsonRpc(responseArg, body.id, { download_rate: 4096, upload_rate: 1024, dht_download_rate: 128, dht_upload_rate: 64 });
|
|
return;
|
|
}
|
|
if (method === 'core.get_session_state') {
|
|
jsonRpc(responseArg, body.id, ['abc']);
|
|
return;
|
|
}
|
|
if (method === 'core.pause_torrent' || method === 'core.resume_torrent' || method === 'web.add_torrents') {
|
|
jsonRpc(responseArg, body.id, true);
|
|
return;
|
|
}
|
|
if (method === 'core.remove_torrent') {
|
|
jsonRpc(responseArg, body.id, true);
|
|
return;
|
|
}
|
|
jsonRpc(responseArg, body.id, null, { message: 'Unknown method', code: 2 });
|
|
});
|
|
|
|
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
const address = server.address();
|
|
const port = typeof address === 'object' && address ? address.port : 0;
|
|
return {
|
|
url: `http://127.0.0.1:${port}`,
|
|
calls,
|
|
close: async () => new Promise<void>((resolve, reject) => server.close((errorArg) => errorArg ? reject(errorArg) : resolve())),
|
|
};
|
|
};
|
|
|
|
tap.test('reads Deluge Web JSON-RPC snapshots and runs controls', async () => {
|
|
const server = await startDelugeWebServer();
|
|
try {
|
|
const client = new DelugeClient({ url: server.url, password: 'deluge', timeoutMs: 1000, name: 'Deluge' });
|
|
const snapshot = await client.getSnapshot(true);
|
|
const runtime = await new DelugeIntegration().setup({ url: server.url, password: 'deluge', timeoutMs: 1000, name: 'Deluge' }, {});
|
|
const entities = await runtime.entities();
|
|
const pause = await runtime.callService?.({ domain: 'switch', service: 'turn_off', target: { entityId: 'switch.deluge_enabled' } });
|
|
const add = await runtime.callService?.({ domain: 'deluge', service: 'add_magnet', target: {}, data: { magnet: 'magnet:?xt=urn:btih:123' } });
|
|
const remove = await runtime.callService?.({ domain: 'deluge', service: 'remove_torrent', target: {}, data: { torrent_id: 'abc', remove_data: true } });
|
|
|
|
expect(snapshot.online).toBeTrue();
|
|
expect(snapshot.source).toEqual('web');
|
|
expect(snapshot.torrents[0].name).toEqual('Ubuntu ISO');
|
|
expect(entities.find((entityArg) => entityArg.id === 'sensor.deluge_download_speed')?.state).toEqual(4);
|
|
expect(pause?.success).toBeTrue();
|
|
expect(add?.success).toBeTrue();
|
|
expect(remove?.success).toBeTrue();
|
|
expect(server.calls.some((callArg) => callArg.method === 'core.pause_torrent')).toBeTrue();
|
|
expect(server.calls.some((callArg) => callArg.method === 'web.add_torrents')).toBeTrue();
|
|
expect(server.calls.some((callArg) => callArg.method === 'core.remove_torrent')).toBeTrue();
|
|
await runtime.destroy();
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
tap.test('validates live Deluge Web credentials during config flow', async () => {
|
|
const server = await startDelugeWebServer();
|
|
try {
|
|
const step = await new DelugeConfigFlow().start({ source: 'manual', metadata: { url: server.url } }, {});
|
|
const done = await step.submit!({ password: 'deluge' });
|
|
|
|
expect(done.kind).toEqual('done');
|
|
expect(done.config?.url).toEqual(server.url);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
tap.test('does not report control success for static Deluge snapshots', async () => {
|
|
const rawData: Partial<IDelugeRawData> = { webUi: { connected: true, stats: {}, torrents: { abc: { name: 'Ubuntu ISO', state: 'Downloading', paused: false } } } };
|
|
const snapshot = DelugeMapper.toSnapshot({ config: { name: 'Static Deluge' }, rawData, online: true, source: 'manual' });
|
|
const runtime = await new DelugeIntegration().setup({ snapshot }, {});
|
|
const result = await runtime.callService?.({ domain: 'switch', service: 'turn_off', target: { entityId: 'switch.static_deluge_enabled' } });
|
|
|
|
expect(result?.success).toBeFalse();
|
|
expect(result?.error).toContain('controls require');
|
|
await runtime.destroy();
|
|
});
|
|
|
|
export default tap.start();
|