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