336 lines
13 KiB
TypeScript
336 lines
13 KiB
TypeScript
import { tap, expect } from '@push.rocks/tapbundle';
|
|
import { NupstSnmp } from '../ts/snmp.js';
|
|
import type { SnmpConfig, UpsStatus } from '../ts/snmp.js';
|
|
import { SnmpEncoder } from '../ts/snmp/encoder.js';
|
|
import { SnmpPacketCreator } from '../ts/snmp/packet-creator.js';
|
|
import { SnmpPacketParser } from '../ts/snmp/packet-parser.js';
|
|
|
|
import * as qenv from '@push.rocks/qenv';
|
|
const testQenv = new qenv.Qenv('./', '.nogit/');
|
|
|
|
// Create an SNMP instance with debug enabled
|
|
const snmp = new NupstSnmp(true);
|
|
|
|
// Load the test configuration from .nogit/env.json
|
|
const testConfig = await testQenv.getEnvVarOnDemandAsObject('testConfig');
|
|
|
|
tap.test('should log config', async () => {
|
|
console.log(testConfig);
|
|
});
|
|
|
|
tap.test('SNMP packet creation and parsing test', async () => {
|
|
// We'll test the internal methods that are now in separate classes
|
|
|
|
// Test OID conversion
|
|
const oidStr = '1.3.6.1.4.1.3808.1.1.1.4.1.1.0';
|
|
const oidArray = SnmpEncoder.oidToArray(oidStr);
|
|
console.log('OID array length:', oidArray.length);
|
|
console.log('OID array:', oidArray);
|
|
// The OID has 14 elements after splitting
|
|
expect(oidArray.length).toEqual(14);
|
|
expect(oidArray[0]).toEqual(1);
|
|
expect(oidArray[1]).toEqual(3);
|
|
|
|
// Test OID encoding
|
|
const encodedOid = SnmpEncoder.encodeOID(oidArray);
|
|
expect(encodedOid).toBeInstanceOf(Buffer);
|
|
|
|
// Test SNMP request creation
|
|
const request = SnmpPacketCreator.createSnmpGetRequest(oidStr, 'public', true);
|
|
expect(request).toBeInstanceOf(Buffer);
|
|
expect(request.length).toBeGreaterThan(20);
|
|
|
|
// Log the request for debugging
|
|
console.log('SNMP Request buffer:', request.toString('hex'));
|
|
|
|
// Test integer encoding
|
|
const int = SnmpEncoder.encodeInteger(42);
|
|
expect(int).toBeInstanceOf(Buffer);
|
|
expect(int.length).toBeGreaterThanOrEqual(1);
|
|
|
|
// Test SNMPv3 engine ID discovery message
|
|
const discoveryMsg = SnmpPacketCreator.createDiscoveryMessage(testConfig, 1);
|
|
expect(discoveryMsg).toBeInstanceOf(Buffer);
|
|
expect(discoveryMsg.length).toBeGreaterThan(20);
|
|
|
|
console.log('SNMPv3 Discovery message:', discoveryMsg.toString('hex'));
|
|
});
|
|
|
|
tap.test('SNMP response parsing simulation', async () => {
|
|
// Create a simulated SNMP response for parsing
|
|
|
|
// Simulate an INTEGER response (battery capacity)
|
|
const intResponse = Buffer.from([
|
|
0x30, 0x29, // Sequence, length 41
|
|
0x02, 0x01, 0x00, // Integer (version), value 0
|
|
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
|
0xa2, 0x1c, // GetResponse
|
|
0x02, 0x01, 0x01, // Integer (request ID), value 1
|
|
0x02, 0x01, 0x00, // Integer (error status), value 0
|
|
0x02, 0x01, 0x00, // Integer (error index), value 0
|
|
0x30, 0x11, // Sequence (varbinds)
|
|
0x30, 0x0f, // Sequence (varbind)
|
|
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x01, 0x00, // OID (example)
|
|
0x02, 0x01, 0x64 // Integer (value), value 100 (100%)
|
|
]);
|
|
|
|
// Simulate a Gauge32 response (battery capacity)
|
|
const gauge32Response = Buffer.from([
|
|
0x30, 0x29, // Sequence, length 41
|
|
0x02, 0x01, 0x00, // Integer (version), value 0
|
|
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
|
0xa2, 0x1c, // GetResponse
|
|
0x02, 0x01, 0x01, // Integer (request ID), value 1
|
|
0x02, 0x01, 0x00, // Integer (error status), value 0
|
|
0x02, 0x01, 0x00, // Integer (error index), value 0
|
|
0x30, 0x11, // Sequence (varbinds)
|
|
0x30, 0x0f, // Sequence (varbind)
|
|
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x01, 0x00, // OID (example)
|
|
0x42, 0x01, 0x64 // Gauge32 (value), value 100 (100%)
|
|
]);
|
|
|
|
// Simulate a TimeTicks response (battery runtime)
|
|
const timeTicksResponse = Buffer.from([
|
|
0x30, 0x29, // Sequence, length 41
|
|
0x02, 0x01, 0x00, // Integer (version), value 0
|
|
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
|
0xa2, 0x1c, // GetResponse
|
|
0x02, 0x01, 0x01, // Integer (request ID), value 1
|
|
0x02, 0x01, 0x00, // Integer (error status), value 0
|
|
0x02, 0x01, 0x00, // Integer (error index), value 0
|
|
0x30, 0x11, // Sequence (varbinds)
|
|
0x30, 0x0f, // Sequence (varbind)
|
|
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x01, 0x00, // OID (example)
|
|
0x43, 0x01, 0x0f // TimeTicks (value), value 15 (0.15 seconds or 15/100 seconds)
|
|
]);
|
|
|
|
// Test parsing INTEGER response
|
|
const intValue = SnmpPacketParser.parseSnmpResponse(intResponse, testConfig, true);
|
|
console.log('Parsed INTEGER value:', intValue);
|
|
expect(intValue).toEqual(100);
|
|
|
|
// Test parsing Gauge32 response
|
|
const gauge32Value = SnmpPacketParser.parseSnmpResponse(gauge32Response, testConfig, true);
|
|
console.log('Parsed Gauge32 value:', gauge32Value);
|
|
expect(gauge32Value).toEqual(100);
|
|
|
|
// Test parsing TimeTicks response
|
|
const timeTicksValue = SnmpPacketParser.parseSnmpResponse(timeTicksResponse, testConfig, true);
|
|
console.log('Parsed TimeTicks value:', timeTicksValue);
|
|
expect(timeTicksValue).toEqual(15);
|
|
});
|
|
|
|
tap.test('CyberPower TimeTicks conversion', async () => {
|
|
// Test the conversion of TimeTicks to minutes for CyberPower UPS
|
|
|
|
// Set up a config for CyberPower
|
|
const cyberPowerConfig: SnmpConfig = {
|
|
...testConfig,
|
|
upsModel: 'cyberpower'
|
|
};
|
|
|
|
// Create a simulated TimeTicks response with a value of 104 (104/100 seconds)
|
|
const ticksResponse = Buffer.from([
|
|
0x30, 0x29, // Sequence
|
|
0x02, 0x01, 0x00, // Integer (version), value 0
|
|
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
|
0xa2, 0x1c, // GetResponse
|
|
0x02, 0x01, 0x01, // Integer (request ID), value 1
|
|
0x02, 0x01, 0x00, // Integer (error status), value 0
|
|
0x02, 0x01, 0x00, // Integer (error index), value 0
|
|
0x30, 0x11, // Sequence (varbinds)
|
|
0x30, 0x0f, // Sequence (varbind)
|
|
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x04, 0x00, // OID (battery runtime)
|
|
0x43, 0x01, 0x68 // TimeTicks (value), value 104 (104/100 seconds)
|
|
]);
|
|
|
|
// Mock the getUpsStatus function to test our TimeTicks conversion logic
|
|
const mockGetUpsStatus = async () => {
|
|
// Parse the TimeTicks value from the response
|
|
const runtime = SnmpPacketParser.parseSnmpResponse(ticksResponse, testConfig, true);
|
|
console.log('Raw runtime value:', runtime);
|
|
|
|
// Create a sample UPS status result
|
|
const result = {
|
|
powerStatus: 'onBattery',
|
|
batteryCapacity: 100,
|
|
batteryRuntime: 0,
|
|
raw: {
|
|
powerStatus: 2,
|
|
batteryCapacity: 100,
|
|
batteryRuntime: runtime,
|
|
},
|
|
};
|
|
|
|
// Convert TimeTicks to minutes for CyberPower
|
|
if (cyberPowerConfig.upsModel === 'cyberpower' && runtime > 0) {
|
|
result.batteryRuntime = Math.floor(runtime / 6000);
|
|
console.log(`Converting CyberPower runtime from ${runtime} ticks to ${result.batteryRuntime} minutes`);
|
|
} else {
|
|
result.batteryRuntime = runtime;
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
// Call our mock function
|
|
const status = await mockGetUpsStatus();
|
|
|
|
// Assert the conversion worked correctly
|
|
console.log('Final status object:', status);
|
|
expect(status.batteryRuntime).toEqual(0); // 104 ticks / 6000 = 0.0173... rounds to 0 minutes
|
|
});
|
|
|
|
tap.test('Simulate fully charged online UPS', async () => {
|
|
// Test a realistic scenario of an online UPS with high battery capacity and ~30 mins runtime
|
|
|
|
// Create simulated responses for power status (online), battery capacity (95%), runtime (30 min)
|
|
|
|
// Power Status = 2 (online for CyberPower)
|
|
const powerStatusResponse = Buffer.from([
|
|
0x30, 0x29, // Sequence
|
|
0x02, 0x01, 0x00, // Integer (version), value 0
|
|
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
|
0xa2, 0x1c, // GetResponse
|
|
0x02, 0x01, 0x01, // Integer (request ID), value 1
|
|
0x02, 0x01, 0x00, // Integer (error status), value 0
|
|
0x02, 0x01, 0x00, // Integer (error index), value 0
|
|
0x30, 0x11, // Sequence (varbinds)
|
|
0x30, 0x0f, // Sequence (varbind)
|
|
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xed, 0x08, 0x01, 0x01, 0x01, 0x00, // OID (power status)
|
|
0x02, 0x01, 0x02 // Integer (value), value 2 (online)
|
|
]);
|
|
|
|
// Battery Capacity = 95% (as Gauge32)
|
|
const batteryCapacityResponse = Buffer.from([
|
|
0x30, 0x29, // Sequence
|
|
0x02, 0x01, 0x00, // Integer (version), value 0
|
|
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
|
0xa2, 0x1c, // GetResponse
|
|
0x02, 0x01, 0x02, // Integer (request ID), value 2
|
|
0x02, 0x01, 0x00, // Integer (error status), value 0
|
|
0x02, 0x01, 0x00, // Integer (error index), value 0
|
|
0x30, 0x11, // Sequence (varbinds)
|
|
0x30, 0x0f, // Sequence (varbind)
|
|
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xed, 0x08, 0x01, 0x02, 0x01, 0x00, // OID (battery capacity)
|
|
0x42, 0x01, 0x5F // Gauge32 (value), value 95 (95%)
|
|
]);
|
|
|
|
// Battery Runtime = 30 minutes (as TimeTicks)
|
|
// 30 minutes = 1800 seconds = 180000 ticks (in 1/100 seconds)
|
|
const batteryRuntimeResponse = Buffer.from([
|
|
0x30, 0x2c, // Sequence
|
|
0x02, 0x01, 0x00, // Integer (version), value 0
|
|
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
|
0xa2, 0x1f, // GetResponse
|
|
0x02, 0x01, 0x03, // Integer (request ID), value 3
|
|
0x02, 0x01, 0x00, // Integer (error status), value 0
|
|
0x02, 0x01, 0x00, // Integer (error index), value 0
|
|
0x30, 0x14, // Sequence (varbinds)
|
|
0x30, 0x12, // Sequence (varbind)
|
|
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xed, 0x08, 0x01, 0x02, 0x04, 0x00, // OID (battery runtime)
|
|
0x43, 0x04, 0x00, 0x02, 0xBF, 0x20 // TimeTicks (value), value 180000 (1800 seconds = 30 minutes)
|
|
]);
|
|
|
|
// Mock the getUpsStatus function to test with our simulated data
|
|
const mockGetUpsStatus = async () => {
|
|
console.log('Simulating UPS status request with synthetic data');
|
|
|
|
// Create a config that specifies this is a CyberPower UPS
|
|
const upsConfig: SnmpConfig = {
|
|
host: '192.168.1.1',
|
|
port: 161,
|
|
version: 1,
|
|
community: 'public',
|
|
timeout: 5000,
|
|
upsModel: 'cyberpower',
|
|
};
|
|
|
|
// Parse each simulated response
|
|
const powerStatus = SnmpPacketParser.parseSnmpResponse(powerStatusResponse, upsConfig, true);
|
|
console.log('Power status value:', powerStatus);
|
|
|
|
const batteryCapacity = SnmpPacketParser.parseSnmpResponse(batteryCapacityResponse, upsConfig, true);
|
|
console.log('Battery capacity value:', batteryCapacity);
|
|
|
|
const batteryRuntime = SnmpPacketParser.parseSnmpResponse(batteryRuntimeResponse, upsConfig, true);
|
|
console.log('Battery runtime value:', batteryRuntime);
|
|
|
|
// Convert TimeTicks to minutes for CyberPower UPSes
|
|
const runtimeMinutes = Math.floor(batteryRuntime / 6000);
|
|
console.log(`Converting ${batteryRuntime} ticks to ${runtimeMinutes} minutes`);
|
|
|
|
// Interpret power status for CyberPower
|
|
// CyberPower: 2=online, 3=on battery
|
|
let powerStatusText: 'online' | 'onBattery' | 'unknown' = 'unknown';
|
|
if (powerStatus === 2) {
|
|
powerStatusText = 'online';
|
|
} else if (powerStatus === 3) {
|
|
powerStatusText = 'onBattery';
|
|
}
|
|
|
|
// Create the status result
|
|
const result: UpsStatus = {
|
|
powerStatus: powerStatusText,
|
|
batteryCapacity: batteryCapacity,
|
|
batteryRuntime: runtimeMinutes,
|
|
raw: {
|
|
powerStatus,
|
|
batteryCapacity,
|
|
batteryRuntime,
|
|
},
|
|
};
|
|
|
|
return result;
|
|
};
|
|
|
|
// Call our mock function
|
|
const status = await mockGetUpsStatus();
|
|
|
|
// Assert that the values match our expectations
|
|
console.log('UPS Status Result:', status);
|
|
expect(status.powerStatus).toEqual('online');
|
|
expect(status.batteryCapacity).toEqual(95);
|
|
expect(status.batteryRuntime).toEqual(30);
|
|
});
|
|
|
|
// Test with real UPS using the configuration from .nogit/env.json
|
|
tap.test('Real UPS test', async () => {
|
|
try {
|
|
console.log('Testing with real UPS configuration...');
|
|
|
|
// Extract the correct SNMP config from the test configuration
|
|
const snmpConfig = testConfig.snmp;
|
|
console.log('SNMP Config:');
|
|
console.log(` Host: ${snmpConfig.host}:${snmpConfig.port}`);
|
|
console.log(` Version: SNMPv${snmpConfig.version}`);
|
|
console.log(` UPS Model: ${snmpConfig.upsModel}`);
|
|
|
|
// Use a short timeout for testing
|
|
const testSnmpConfig = {
|
|
...snmpConfig,
|
|
timeout: Math.min(snmpConfig.timeout, 10000) // Use at most 10 seconds for testing
|
|
};
|
|
|
|
// Try to get the UPS status
|
|
const status = await snmp.getUpsStatus(testSnmpConfig);
|
|
|
|
console.log('UPS Status:');
|
|
console.log(` Power Status: ${status.powerStatus}`);
|
|
console.log(` Battery Capacity: ${status.batteryCapacity}%`);
|
|
console.log(` Runtime Remaining: ${status.batteryRuntime} minutes`);
|
|
|
|
// Just make sure we got valid data types back
|
|
expect(status).toBeTruthy();
|
|
expect(['online', 'onBattery', 'unknown']).toContain(status.powerStatus);
|
|
expect(typeof status.batteryCapacity).toEqual('number');
|
|
expect(typeof status.batteryRuntime).toEqual('number');
|
|
} catch (error) {
|
|
console.log('Real UPS test failed:', error);
|
|
// Skip the test if we can't connect to the real UPS
|
|
console.log('Skipping this test since the UPS might not be available');
|
|
}
|
|
});
|
|
|
|
// Export the default tap object
|
|
export default tap.start(); |