Files
integrations/test/pi_hole/test.pi_hole.runtime.node.ts

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();