113 lines
4.8 KiB
TypeScript
113 lines
4.8 KiB
TypeScript
import { createServer } from 'node:http';
|
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import { PiHoleClient, PiHoleIntegration, type IPiHoleSnapshot } from '../../ts/integrations/pi_hole/index.js';
|
|
|
|
const snapshot: IPiHoleSnapshot = {
|
|
online: true,
|
|
apiVersion: 6,
|
|
status: 'enabled',
|
|
statistics: {
|
|
adsBlocked: 1,
|
|
adsPercentage: 10,
|
|
clientsSeen: 1,
|
|
dnsQueries: 10,
|
|
domainsBlocked: 100,
|
|
queriesCached: 2,
|
|
queriesForwarded: 7,
|
|
uniqueClients: 1,
|
|
uniqueDomains: 8,
|
|
},
|
|
versions: { core: {}, web: {}, ftl: {} },
|
|
name: 'Pi-hole',
|
|
};
|
|
|
|
tap.test('reads Pi-hole v6 local HTTP API and executes disable through HTTP', async () => {
|
|
const requests: Array<{ method?: string; url?: string; sid?: string; csrf?: string; body?: string }> = [];
|
|
const server = createServer((requestArg, responseArg) => {
|
|
let body = '';
|
|
requestArg.on('data', (chunkArg) => { body += String(chunkArg); });
|
|
requestArg.on('end', () => {
|
|
requests.push({
|
|
method: requestArg.method,
|
|
url: requestArg.url,
|
|
sid: requestArg.headers['x-ftl-sid'] as string | undefined,
|
|
csrf: requestArg.headers['x-ftl-csrf'] as string | undefined,
|
|
body,
|
|
});
|
|
responseArg.setHeader('content-type', 'application/json');
|
|
if (requestArg.method === 'POST' && requestArg.url === '/api/auth') {
|
|
responseArg.end(JSON.stringify({ session: { valid: true, sid: 'sid-1', csrf: 'csrf-1', validity: 300 } }));
|
|
return;
|
|
}
|
|
if (requestArg.method === 'GET' && requestArg.url === '/api/stats/summary') {
|
|
responseArg.end(JSON.stringify({ queries: { blocked: 5, percent_blocked: 25, total: 20, cached: 4, forwarded: 10, unique_domains: 12 }, clients: { total: 3, active: 2 }, gravity: { domains_being_blocked: 100000 } }));
|
|
return;
|
|
}
|
|
if (requestArg.method === 'GET' && requestArg.url === '/api/dns/blocking') {
|
|
responseArg.end(JSON.stringify({ blocking: 'enabled' }));
|
|
return;
|
|
}
|
|
if (requestArg.method === 'GET' && requestArg.url === '/api/info/version') {
|
|
responseArg.end(JSON.stringify({ version: { core: { local: { version: 'v6.0', hash: 'a' }, remote: { version: 'v6.0', hash: 'a' } }, web: {}, ftl: {} } }));
|
|
return;
|
|
}
|
|
if (requestArg.method === 'POST' && requestArg.url === '/api/dns/blocking') {
|
|
responseArg.end(JSON.stringify({ blocking: false, timer: 15 }));
|
|
return;
|
|
}
|
|
responseArg.statusCode = 404;
|
|
responseArg.end(JSON.stringify({ error: 'not found' }));
|
|
});
|
|
});
|
|
|
|
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
try {
|
|
const address = server.address();
|
|
const port = typeof address === 'object' && address ? address.port : 0;
|
|
const runtime = await new PiHoleIntegration().setup({ host: '127.0.0.1', port, apiKey: 'secret', apiVersion: 6, timeoutMs: 1000 }, {});
|
|
const entities = await runtime.entities();
|
|
const result = await runtime.callService?.({
|
|
domain: 'switch',
|
|
service: 'turn_off',
|
|
target: { entityId: entities.find((entityArg) => entityArg.platform === 'switch')?.id },
|
|
data: { duration: '00:00:15' },
|
|
});
|
|
|
|
const disableRequest = requests.find((requestArg) => requestArg.method === 'POST' && requestArg.url === '/api/dns/blocking');
|
|
expect(entities.find((entityArg) => entityArg.id === 'sensor.pi_hole_dns_queries')?.state).toEqual(20);
|
|
expect(result?.success).toBeTrue();
|
|
expect(disableRequest?.sid).toEqual('sid-1');
|
|
expect(disableRequest?.csrf).toEqual('csrf-1');
|
|
expect(JSON.parse(disableRequest?.body || '{}')).toEqual({ blocking: false, timer: 15 });
|
|
await runtime.destroy();
|
|
} finally {
|
|
await new Promise<void>((resolve, reject) => server.close((errorArg) => errorArg ? reject(errorArg) : resolve()));
|
|
}
|
|
});
|
|
|
|
tap.test('does not report snapshot-only Pi-hole commands as successful', async () => {
|
|
const runtime = await new PiHoleIntegration().setup({ snapshot }, {});
|
|
const switchEntity = (await runtime.entities()).find((entityArg) => entityArg.platform === 'switch');
|
|
const result = await runtime.callService?.({
|
|
domain: 'switch',
|
|
service: 'turn_off',
|
|
target: { entityId: switchEntity?.id },
|
|
});
|
|
|
|
expect(result?.success).toBeFalse();
|
|
expect(result?.error).toEqual('Pi-hole live commands require config.host or commandExecutor; snapshot-only commands are not reported as successful.');
|
|
await runtime.destroy();
|
|
});
|
|
|
|
tap.test('does not fake refresh success without HTTP endpoint or snapshot', async () => {
|
|
const client = new PiHoleClient({});
|
|
const snapshotResult = await client.getSnapshot();
|
|
const refreshResult = await client.refresh();
|
|
|
|
expect(snapshotResult.online).toBeFalse();
|
|
expect(refreshResult.success).toBeFalse();
|
|
expect(refreshResult.error).toContain('endpoint');
|
|
});
|
|
|
|
export default tap.start();
|