Add native local NAS and network service integrations
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { HomeAssistantOpnsenseIntegration, type IOpnsenseCommand, type IOpnsenseConfig } from '../../ts/integrations/opnsense/index.js';
|
||||
|
||||
const config: IOpnsenseConfig = {
|
||||
url: 'https://192.168.1.1',
|
||||
apiKey: 'key',
|
||||
apiSecret: 'secret',
|
||||
snapshot: {
|
||||
connected: true,
|
||||
router: {
|
||||
host: '192.168.1.1',
|
||||
name: 'Edge Firewall',
|
||||
macAddress: 'AA:BB:CC:DD:EE:FF',
|
||||
actions: ['reboot'],
|
||||
},
|
||||
devices: [],
|
||||
interfaces: [],
|
||||
gateways: [],
|
||||
firewall: {},
|
||||
system: {},
|
||||
telemetry: {},
|
||||
services: [],
|
||||
vpn: {},
|
||||
sensors: {},
|
||||
switches: [],
|
||||
},
|
||||
};
|
||||
|
||||
tap.test('does not fake OPNsense live API command success without executor', async () => {
|
||||
const runtime = await new HomeAssistantOpnsenseIntegration().setup(config, {});
|
||||
const result = await runtime.callService!({ domain: 'opnsense', service: 'reboot', target: {} });
|
||||
|
||||
expect(result.success).toBeFalse();
|
||||
expect(result.error || '').toInclude('not faked');
|
||||
await runtime.destroy();
|
||||
});
|
||||
|
||||
tap.test('executes represented OPNsense commands through injected executor', async () => {
|
||||
let command: IOpnsenseCommand | undefined;
|
||||
const runtime = await new HomeAssistantOpnsenseIntegration().setup({
|
||||
...config,
|
||||
commandExecutor: async (commandArg) => {
|
||||
command = commandArg;
|
||||
return { success: true, data: { accepted: true } };
|
||||
},
|
||||
}, {});
|
||||
const result = await runtime.callService!({ domain: 'opnsense', service: 'reboot', target: {} });
|
||||
|
||||
expect(result.success).toBeTrue();
|
||||
expect(command?.type).toEqual('router.action');
|
||||
expect(command?.path).toEqual('/api/core/system/reboot');
|
||||
await runtime.destroy();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,66 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { OpnsenseConfigFlow, createOpnsenseDiscoveryDescriptor, type IOpnsenseSnapshot } from '../../ts/integrations/opnsense/index.js';
|
||||
|
||||
const snapshot: IOpnsenseSnapshot = {
|
||||
connected: true,
|
||||
router: { name: 'Snapshot Firewall', macAddress: 'AA:BB:CC:DD:EE:FF' },
|
||||
devices: [],
|
||||
interfaces: [],
|
||||
gateways: [],
|
||||
firewall: {},
|
||||
system: {},
|
||||
telemetry: {},
|
||||
services: [],
|
||||
vpn: {},
|
||||
sensors: {},
|
||||
switches: [],
|
||||
};
|
||||
|
||||
tap.test('matches and validates manual local HTTPS OPNsense candidates', async () => {
|
||||
const descriptor = createOpnsenseDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers()[0];
|
||||
const result = await matcher.matches({
|
||||
url: 'https://firewall.local:8443',
|
||||
name: 'OPNsense Firewall',
|
||||
model: 'OPNsense',
|
||||
macAddress: 'AA-BB-CC-DD-EE-FF',
|
||||
}, {});
|
||||
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.candidate?.integrationDomain).toEqual('opnsense');
|
||||
expect(result.candidate?.port).toEqual(8443);
|
||||
expect(result.normalizedDeviceId).toEqual('aa:bb:cc:dd:ee:ff');
|
||||
expect(result.candidate?.metadata?.verifySsl).toBeFalse();
|
||||
|
||||
const validator = descriptor.getValidators()[0];
|
||||
const validation = await validator.validate(result.candidate!, {});
|
||||
expect(validation.matched).toBeTrue();
|
||||
expect(validation.metadata?.liveHttpImplemented).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('accepts snapshot-only setup and rejects non-HTTPS endpoints', async () => {
|
||||
const descriptor = createOpnsenseDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers()[0];
|
||||
const snapshotResult = await matcher.matches({ snapshot }, {});
|
||||
const httpResult = await matcher.matches({ url: 'http://firewall.local', name: 'OPNsense' }, {});
|
||||
|
||||
expect(snapshotResult.matched).toBeTrue();
|
||||
expect(snapshotResult.confidence).toEqual('certain');
|
||||
expect(httpResult.matched).toBeFalse();
|
||||
expect(httpResult.reason).toInclude('HTTPS');
|
||||
});
|
||||
|
||||
tap.test('builds OPNsense config flow output without claiming live HTTP support', async () => {
|
||||
const flow = new OpnsenseConfigFlow();
|
||||
const step = await flow.start({ source: 'manual', host: 'firewall.local', metadata: { trackerInterfaces: ['LAN'] } }, {});
|
||||
const done = await step.submit!({ url: 'firewall.local', apiKey: 'key', apiSecret: 'secret', trackerInterfaces: 'LAN,WAN' });
|
||||
|
||||
expect(done.kind).toEqual('done');
|
||||
expect(done.config?.url).toEqual('https://firewall.local');
|
||||
expect(done.config?.ssl).toBeTrue();
|
||||
expect(done.config?.verifySsl).toBeFalse();
|
||||
expect(done.config?.trackerInterfaces).toEqual(['LAN', 'WAN']);
|
||||
expect(done.config?.metadata?.liveHttpImplemented).toBeFalse();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,127 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { OpnsenseMapper, type IOpnsenseSnapshot } from '../../ts/integrations/opnsense/index.js';
|
||||
|
||||
const snapshot: IOpnsenseSnapshot = {
|
||||
connected: true,
|
||||
updatedAt: '2026-01-01T00:00:00.000Z',
|
||||
router: {
|
||||
host: '192.168.1.1',
|
||||
name: 'Edge Firewall',
|
||||
macAddress: 'AA:BB:CC:DD:EE:FF',
|
||||
firmware: '25.7',
|
||||
latestFirmware: '26.1',
|
||||
updateAvailable: true,
|
||||
actions: ['reboot', 'firmware_update'],
|
||||
},
|
||||
devices: [],
|
||||
interfaces: [
|
||||
{
|
||||
name: 'wan',
|
||||
label: 'WAN',
|
||||
status: 'up',
|
||||
inbytes: 2_000_000_000,
|
||||
outbytes: 1_000_000_000,
|
||||
inpkts: 100,
|
||||
outpkts: 50,
|
||||
actions: ['reload'],
|
||||
},
|
||||
],
|
||||
gateways: [
|
||||
{ name: 'WAN_DHCP', status: 'online', latency: '4.1 ms', loss: '0.0 %', interface: 'wan' },
|
||||
],
|
||||
firewall: {
|
||||
rules: [{ uuid: 'rule-1', description: 'Allow LAN', enabled: true, interface: 'lan', protocol: 'tcp' }],
|
||||
nat: { d_nat: [{ uuid: 'nat-1', description: 'HTTPS', disabled: '0', protocol: 'tcp' }] },
|
||||
aliases: [{ uuid: 'alias-1', name: 'BlockedHosts', enabled: false, type: 'host' }],
|
||||
state: { used: 42, total: 100, usedPercent: 42 },
|
||||
},
|
||||
system: {
|
||||
firmwareVersion: '25.7',
|
||||
productLatest: '26.1',
|
||||
updateAvailable: true,
|
||||
pendingNoticesPresent: true,
|
||||
pendingNotices: [{ id: 'notice1', notice: 'Reboot required' }],
|
||||
},
|
||||
telemetry: {
|
||||
cpu: { usage_total: 12 },
|
||||
memory: { used_percent: 34 },
|
||||
mbuf: { used_percent: 5 },
|
||||
},
|
||||
services: [
|
||||
{ name: 'unbound', displayName: 'Unbound DNS', running: 1 },
|
||||
],
|
||||
vpn: {
|
||||
openvpn: { servers: [{ uuid: 'ovpn1', name: 'Remote Access', enabled: true, connected_clients: 2 }] },
|
||||
wireguard: { clients: [{ uuid: 'wgclient1', name: 'Mullvad', enabled: false, connected_servers: 0 }] },
|
||||
},
|
||||
sensors: {},
|
||||
switches: [
|
||||
{ id: 'dnsbl', name: 'Unbound DNSBL', enabled: true, nativeType: 'unbound_blocklist', uuid: 'dnsbl-1' },
|
||||
],
|
||||
actions: [],
|
||||
};
|
||||
|
||||
tap.test('maps OPNsense snapshot sections and HA ARP tracker filtering', async () => {
|
||||
const normalized = OpnsenseMapper.toSnapshot({
|
||||
snapshot,
|
||||
trackerInterfaces: ['LAN'],
|
||||
arpTable: [
|
||||
{ 'mac-address': '11:22:33:44:55:66', 'ip-address': '192.168.1.20', hostname: 'Kitchen Phone', intf_description: 'LAN', manufacturer: 'PhoneCo' },
|
||||
{ 'mac-address': '22:33:44:55:66:77', 'ip-address': '192.168.1.21', hostname: 'WAN Host', intf_description: 'WAN' },
|
||||
],
|
||||
});
|
||||
const devices = OpnsenseMapper.toDevices(normalized);
|
||||
const entities = OpnsenseMapper.toEntities(normalized);
|
||||
|
||||
expect(devices.some((deviceArg) => deviceArg.id === 'opnsense.router.aa_bb_cc_dd_ee_ff')).toBeTrue();
|
||||
expect(devices.some((deviceArg) => deviceArg.id === 'opnsense.client.11_22_33_44_55_66')).toBeTrue();
|
||||
expect(devices.some((deviceArg) => deviceArg.id === 'opnsense.client.22_33_44_55_66_77')).toBeFalse();
|
||||
expect(entities.find((entityArg) => entityArg.id === 'sensor.edge_firewall_cpu_usage')?.state).toEqual(12);
|
||||
expect(entities.find((entityArg) => entityArg.id === 'sensor.edge_firewall_memory_usage')?.state).toEqual(34);
|
||||
expect(entities.find((entityArg) => entityArg.id === 'sensor.edge_firewall_firewall_state_table_used')?.state).toEqual(42);
|
||||
expect(entities.find((entityArg) => entityArg.id === 'sensor.edge_firewall_wan_download')?.state).toEqual(2);
|
||||
expect(entities.find((entityArg) => entityArg.id === 'binary_sensor.edge_firewall_gateway_wan_dhcp')?.state).toEqual('on');
|
||||
expect(entities.find((entityArg) => entityArg.id === 'binary_sensor.kitchen_phone_connected')?.attributes?.nativePlatform).toEqual('device_tracker');
|
||||
expect(entities.find((entityArg) => entityArg.id === 'switch.edge_firewall_firewall_allow_lan')?.state).toEqual('on');
|
||||
expect(entities.find((entityArg) => entityArg.id === 'switch.unbound_dns_service')?.state).toEqual('on');
|
||||
expect(entities.find((entityArg) => entityArg.id === 'switch.edge_firewall_openvpn_remote_access')?.state).toEqual('on');
|
||||
expect(entities.find((entityArg) => entityArg.id === 'update.edge_firewall_firmware')?.attributes?.latestVersion).toEqual('26.1');
|
||||
});
|
||||
|
||||
tap.test('models safe OPNsense commands only for represented resources', async () => {
|
||||
const normalized = OpnsenseMapper.toSnapshot({ snapshot });
|
||||
const serviceCommand = OpnsenseMapper.commandForService(normalized, {
|
||||
domain: 'switch',
|
||||
service: 'turn_off',
|
||||
target: { entityId: 'switch.unbound_dns_service' },
|
||||
});
|
||||
const firewallCommand = OpnsenseMapper.commandForService(normalized, {
|
||||
domain: 'switch',
|
||||
service: 'turn_off',
|
||||
target: { entityId: 'switch.edge_firewall_firewall_allow_lan' },
|
||||
});
|
||||
const natCommand = OpnsenseMapper.commandForService(normalized, {
|
||||
domain: 'switch',
|
||||
service: 'turn_off',
|
||||
target: { entityId: 'switch.edge_firewall_nat_https' },
|
||||
});
|
||||
const rebootCommand = OpnsenseMapper.commandForService(normalized, {
|
||||
domain: 'opnsense',
|
||||
service: 'reboot',
|
||||
target: {},
|
||||
});
|
||||
const missingServiceCommand = OpnsenseMapper.commandForService(normalized, {
|
||||
domain: 'opnsense',
|
||||
service: 'stop_service',
|
||||
target: {},
|
||||
data: { service: 'missing' },
|
||||
});
|
||||
|
||||
expect(serviceCommand && !('error' in serviceCommand) ? serviceCommand.path : '').toEqual('/api/core/service/stop/unbound');
|
||||
expect(firewallCommand && !('error' in firewallCommand) ? firewallCommand.path : '').toEqual('/api/firewall/filter/toggle_rule/rule-1/0');
|
||||
expect(natCommand && !('error' in natCommand) ? natCommand.path : '').toEqual('/api/firewall/d_nat/toggle_rule/nat-1/1');
|
||||
expect(rebootCommand && !('error' in rebootCommand) ? rebootCommand.type : '').toEqual('router.action');
|
||||
expect(missingServiceCommand).toBeUndefined();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user