]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
test: add GetVariables command UTs
authorJérôme Benoit <jerome.benoit@sap.com>
Wed, 22 Oct 2025 08:46:30 +0000 (10:46 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Wed, 22 Oct 2025 08:46:30 +0000 (10:46 +0200)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetVariables.test.ts [new file with mode: 0644]
tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts [new file with mode: 0644]

diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetVariables.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetVariables.test.ts
new file mode 100644 (file)
index 0000000..fc7a9d9
--- /dev/null
@@ -0,0 +1,199 @@
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+
+import { expect } from '@std/expect'
+import { describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../../../src/charging-station/index.js'
+
+import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.js'
+import {
+  AttributeEnumType,
+  GetVariableStatusEnumType,
+  OCPP20ComponentName,
+  OCPP20ConnectorStatusEnumType,
+  type OCPP20GetVariablesRequest,
+  OCPP20OptionalVariableName,
+  OCPP20RequiredVariableName,
+} from '../../../../src/types/index.js'
+
+await describe('OCPP20IncomingRequestService GetVariables integration tests', async () => {
+  // Mock ChargingStation with comprehensive properties
+  const mockChargingStation = {
+    connectors: new Map([
+      [1, { status: OCPP20ConnectorStatusEnumType.Available }],
+      [2, { status: OCPP20ConnectorStatusEnumType.Available }],
+    ]),
+    evses: new Map([
+      [1, { connectors: new Map([[1, {}]]) }],
+      [2, { connectors: new Map([[1, {}]]) }],
+    ]),
+    getHeartbeatInterval: () => 60,
+    hasEvses: true,
+    logPrefix: () => 'CS-TEST-001',
+    ocppConfiguration: {
+      configurationKey: [
+        { key: OCPP20OptionalVariableName.WebSocketPingInterval, value: '30' },
+        { key: OCPP20OptionalVariableName.HeartbeatInterval, value: '60' },
+      ],
+    },
+    stationInfo: {
+      heartbeatInterval: 60,
+      ocppStrictCompliance: false,
+    },
+  } as unknown as ChargingStation
+
+  const incomingRequestService = new OCPP20IncomingRequestService()
+
+  await it('Should handle GetVariables request with valid variables', async () => {
+    const request: OCPP20GetVariablesRequest = {
+      getVariableData: [
+        {
+          attributeType: AttributeEnumType.Actual,
+          component: { name: OCPP20ComponentName.ChargingStation },
+          variable: { name: OCPP20OptionalVariableName.HeartbeatInterval },
+        },
+        {
+          component: { name: OCPP20ComponentName.ChargingStation },
+          variable: { name: OCPP20OptionalVariableName.WebSocketPingInterval },
+        },
+      ],
+    }
+
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+    const response = await (incomingRequestService as any).handleRequestGetVariables(
+      mockChargingStation,
+      request
+    )
+
+    expect(response).toBeDefined()
+    expect(response.getVariableResult).toBeDefined()
+    expect(Array.isArray(response.getVariableResult)).toBe(true)
+    expect(response.getVariableResult).toHaveLength(2)
+
+    // Check first variable (HeartbeatInterval)
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const firstResult = response.getVariableResult[0]
+    expect(firstResult.attributeStatus).toBe(GetVariableStatusEnumType.Accepted)
+    expect(firstResult.attributeValue).toBe('60')
+    expect(firstResult.component.name).toBe(OCPP20ComponentName.ChargingStation)
+    expect(firstResult.variable.name).toBe(OCPP20OptionalVariableName.HeartbeatInterval)
+
+    // Check second variable (WebSocketPingInterval)
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const secondResult = response.getVariableResult[1]
+    expect(secondResult.attributeStatus).toBe(GetVariableStatusEnumType.Accepted)
+    expect(secondResult.attributeValue).toBe('30')
+    expect(secondResult.component.name).toBe(OCPP20ComponentName.ChargingStation)
+    expect(secondResult.variable.name).toBe(OCPP20OptionalVariableName.WebSocketPingInterval)
+  })
+
+  await it('Should handle GetVariables request with invalid variables', async () => {
+    const request: OCPP20GetVariablesRequest = {
+      getVariableData: [
+        {
+          component: { name: OCPP20ComponentName.ChargingStation },
+          // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
+          variable: { name: 'InvalidVariable' as any },
+        },
+        {
+          // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
+          component: { name: 'InvalidComponent' as any },
+          variable: { name: OCPP20OptionalVariableName.HeartbeatInterval },
+        },
+      ],
+    }
+
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+    const response = await (incomingRequestService as any).handleRequestGetVariables(
+      mockChargingStation,
+      request
+    )
+
+    expect(response).toBeDefined()
+    expect(response.getVariableResult).toBeDefined()
+    expect(Array.isArray(response.getVariableResult)).toBe(true)
+    expect(response.getVariableResult).toHaveLength(2)
+
+    // Check first variable (should be UnknownVariable)
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const firstResult = response.getVariableResult[0]
+    expect(firstResult.attributeStatus).toBe(GetVariableStatusEnumType.UnknownVariable)
+
+    // Check second variable (should be UnknownComponent)
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const secondResult = response.getVariableResult[1]
+    expect(secondResult.attributeStatus).toBe(GetVariableStatusEnumType.UnknownComponent)
+  })
+
+  await it('Should handle GetVariables request with unsupported attribute types', async () => {
+    const request: OCPP20GetVariablesRequest = {
+      getVariableData: [
+        {
+          attributeType: AttributeEnumType.Target, // Not supported for HeartbeatInterval
+          component: { name: OCPP20ComponentName.ChargingStation },
+          variable: { name: OCPP20OptionalVariableName.HeartbeatInterval },
+        },
+      ],
+    }
+
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+    const response = await (incomingRequestService as any).handleRequestGetVariables(
+      mockChargingStation,
+      request
+    )
+
+    expect(response).toBeDefined()
+    expect(response.getVariableResult).toBeDefined()
+    expect(Array.isArray(response.getVariableResult)).toBe(true)
+    expect(response.getVariableResult).toHaveLength(1)
+
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const result = response.getVariableResult[0]
+    expect(result.attributeStatus).toBe(GetVariableStatusEnumType.NotSupportedAttributeType)
+  })
+
+  await it('Should handle GetVariables request with Connector components', async () => {
+    const request: OCPP20GetVariablesRequest = {
+      getVariableData: [
+        {
+          component: {
+            instance: '1',
+            name: OCPP20ComponentName.Connector,
+          },
+          variable: { name: OCPP20RequiredVariableName.AuthorizeRemoteStart },
+        },
+        {
+          component: {
+            instance: '999', // Non-existent connector
+            name: OCPP20ComponentName.Connector,
+          },
+          variable: { name: OCPP20RequiredVariableName.AuthorizeRemoteStart },
+        },
+      ],
+    }
+
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+    const response = await (incomingRequestService as any).handleRequestGetVariables(
+      mockChargingStation,
+      request
+    )
+
+    expect(response).toBeDefined()
+    expect(response.getVariableResult).toBeDefined()
+    expect(Array.isArray(response.getVariableResult)).toBe(true)
+    expect(response.getVariableResult).toHaveLength(2)
+
+    // Check valid connector
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const firstResult = response.getVariableResult[0]
+    expect(firstResult.attributeStatus).toBe(GetVariableStatusEnumType.Accepted)
+    expect(firstResult.component.name).toBe(OCPP20ComponentName.Connector)
+    expect(firstResult.component.instance).toBe('1')
+
+    // Check invalid connector
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const secondResult = response.getVariableResult[1]
+    expect(secondResult.attributeStatus).toBe(GetVariableStatusEnumType.UnknownComponent)
+    expect(secondResult.component.instance).toBe('999')
+  })
+})
diff --git a/tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts b/tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts
new file mode 100644 (file)
index 0000000..b083892
--- /dev/null
@@ -0,0 +1,305 @@
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+
+import { expect } from '@std/expect'
+import { describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../../../src/charging-station/index.js'
+
+import { OCPP20VariableManager } from '../../../../src/charging-station/ocpp/2.0/OCPP20VariableManager.js'
+import {
+  AttributeEnumType,
+  type ComponentType,
+  GetVariableStatusEnumType,
+  OCPP20ComponentName,
+  OCPP20ConnectorStatusEnumType,
+  type OCPP20GetVariableDataType,
+  OCPP20OptionalVariableName,
+  OCPP20RequiredVariableName,
+  type VariableType,
+} from '../../../../src/types/index.js'
+
+await describe('OCPP20VariableManager test suite', async () => {
+  // Mock ChargingStation with basic properties
+  const mockChargingStation = {
+    connectors: new Map([
+      [1, { status: OCPP20ConnectorStatusEnumType.Available }],
+      [2, { status: OCPP20ConnectorStatusEnumType.Available }],
+    ]),
+    evses: new Map([
+      [1, { connectors: new Map([[1, {}]]) }],
+      [2, { connectors: new Map([[1, {}]]) }],
+    ]),
+    getHeartbeatInterval: () => 60,
+    hasEvses: true,
+    logPrefix: () => 'CS-TEST-001',
+    ocppConfiguration: {
+      configurationKey: [
+        { key: OCPP20OptionalVariableName.WebSocketPingInterval, value: '30' },
+        { key: OCPP20OptionalVariableName.HeartbeatInterval, value: '60' },
+      ],
+    },
+    stationInfo: {
+      heartbeatInterval: 60,
+    },
+    wsConnection: {
+      pingInterval: 30,
+    },
+  } as unknown as ChargingStation
+
+  await it('Verify that OCPP20VariableManager can be instantiated as singleton', () => {
+    const manager1 = OCPP20VariableManager.getInstance()
+    const manager2 = OCPP20VariableManager.getInstance()
+
+    expect(manager1).toBeDefined()
+    expect(manager1).toBe(manager2) // Same instance (singleton)
+  })
+
+  await describe('getVariables method tests', async () => {
+    const manager = OCPP20VariableManager.getInstance()
+
+    await it('Should handle valid ChargingStation component requests', () => {
+      const request: OCPP20GetVariableDataType[] = [
+        {
+          attributeType: AttributeEnumType.Actual,
+          component: { name: OCPP20ComponentName.ChargingStation },
+          variable: { name: OCPP20OptionalVariableName.HeartbeatInterval },
+        },
+      ]
+
+      const result = manager.getVariables(mockChargingStation, request)
+
+      expect(Array.isArray(result)).toBe(true)
+      expect(result).toHaveLength(1)
+      expect(result[0].attributeStatus).toBe(GetVariableStatusEnumType.Accepted)
+      expect(result[0].attributeValue).toBe('60')
+      expect(result[0].component.name).toBe(OCPP20ComponentName.ChargingStation)
+      expect(result[0].variable.name).toBe(OCPP20OptionalVariableName.HeartbeatInterval)
+    })
+
+    await it('Should handle valid Connector component requests', () => {
+      const request: OCPP20GetVariableDataType[] = [
+        {
+          component: {
+            instance: '1',
+            name: OCPP20ComponentName.Connector,
+          },
+          variable: { name: OCPP20RequiredVariableName.AuthorizeRemoteStart },
+        },
+      ]
+
+      const result = manager.getVariables(mockChargingStation, request)
+
+      expect(Array.isArray(result)).toBe(true)
+      expect(result).toHaveLength(1)
+      expect(result[0].attributeStatus).toBe(GetVariableStatusEnumType.Accepted)
+      expect(result[0].component.name).toBe(OCPP20ComponentName.Connector)
+      expect(result[0].component.instance).toBe('1')
+    })
+
+    await it('Should handle invalid component gracefully', () => {
+      const request: OCPP20GetVariableDataType[] = [
+        {
+          // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
+          component: { name: 'InvalidComponent' as any },
+          // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
+          variable: { name: 'SomeVariable' as any },
+        },
+      ]
+
+      const result = manager.getVariables(mockChargingStation, request)
+
+      expect(Array.isArray(result)).toBe(true)
+      expect(result).toHaveLength(1)
+      expect(result[0].attributeStatus).toBe(GetVariableStatusEnumType.UnknownComponent)
+    })
+
+    await it('Should handle invalid variable gracefully', () => {
+      const request: OCPP20GetVariableDataType[] = [
+        {
+          component: { name: OCPP20ComponentName.ChargingStation },
+          // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
+          variable: { name: 'InvalidVariable' as any },
+        },
+      ]
+
+      const result = manager.getVariables(mockChargingStation, request)
+
+      expect(Array.isArray(result)).toBe(true)
+      expect(result).toHaveLength(1)
+      expect(result[0].attributeStatus).toBe(GetVariableStatusEnumType.UnknownVariable)
+    })
+
+    await it('Should handle unsupported attribute type gracefully', () => {
+      const request: OCPP20GetVariableDataType[] = [
+        {
+          attributeType: AttributeEnumType.Target, // Not supported for this variable
+          component: { name: OCPP20ComponentName.ChargingStation },
+          variable: { name: OCPP20OptionalVariableName.HeartbeatInterval },
+        },
+      ]
+
+      const result = manager.getVariables(mockChargingStation, request)
+
+      expect(Array.isArray(result)).toBe(true)
+      expect(result).toHaveLength(1)
+      expect(result[0].attributeStatus).toBe(GetVariableStatusEnumType.NotSupportedAttributeType)
+    })
+
+    await it('Should handle non-existent connector instance', () => {
+      const request: OCPP20GetVariableDataType[] = [
+        {
+          component: {
+            instance: '999', // Non-existent connector
+            name: OCPP20ComponentName.Connector,
+          },
+          variable: { name: OCPP20RequiredVariableName.AuthorizeRemoteStart },
+        },
+      ]
+
+      const result = manager.getVariables(mockChargingStation, request)
+
+      expect(Array.isArray(result)).toBe(true)
+      expect(result).toHaveLength(1)
+      expect(result[0].attributeStatus).toBe(GetVariableStatusEnumType.UnknownComponent)
+    })
+
+    await it('Should handle multiple variables in single request', () => {
+      const request: OCPP20GetVariableDataType[] = [
+        {
+          component: { name: OCPP20ComponentName.ChargingStation },
+          variable: { name: OCPP20OptionalVariableName.HeartbeatInterval },
+        },
+        {
+          component: { name: OCPP20ComponentName.ChargingStation },
+          variable: { name: OCPP20OptionalVariableName.WebSocketPingInterval },
+        },
+      ]
+
+      const result = manager.getVariables(mockChargingStation, request)
+
+      expect(Array.isArray(result)).toBe(true)
+      expect(result).toHaveLength(2)
+      expect(result[0].attributeStatus).toBe(GetVariableStatusEnumType.Accepted)
+      expect(result[1].attributeStatus).toBe(GetVariableStatusEnumType.Accepted)
+    })
+
+    await it('Should handle EVSE component when supported', () => {
+      const request: OCPP20GetVariableDataType[] = [
+        {
+          component: {
+            instance: '1',
+            name: OCPP20ComponentName.EVSE,
+          },
+          variable: { name: OCPP20RequiredVariableName.AuthorizeRemoteStart },
+        },
+      ]
+
+      const result = manager.getVariables(mockChargingStation, request)
+
+      expect(Array.isArray(result)).toBe(true)
+      expect(result).toHaveLength(1)
+      // Should be accepted since mockChargingStation has EVSEs
+      expect(result[0].attributeStatus).toBe(GetVariableStatusEnumType.Accepted)
+      expect(result[0].component.name).toBe(OCPP20ComponentName.EVSE)
+    })
+  })
+
+  await describe('Component validation tests', async () => {
+    const manager = OCPP20VariableManager.getInstance()
+
+    await it('Should validate ChargingStation component as always valid', () => {
+      const component: ComponentType = { name: OCPP20ComponentName.ChargingStation }
+
+      // Access private method through any casting for testing
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+      const isValid = (manager as any).isComponentValid(mockChargingStation, component)
+      expect(isValid).toBe(true)
+    })
+
+    await it('Should validate Connector component when connectors exist', () => {
+      const component: ComponentType = { instance: '1', name: OCPP20ComponentName.Connector }
+
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+      const isValid = (manager as any).isComponentValid(mockChargingStation, component)
+      expect(isValid).toBe(true)
+    })
+
+    await it('Should reject invalid connector instance', () => {
+      const component: ComponentType = { instance: '999', name: OCPP20ComponentName.Connector }
+
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+      const isValid = (manager as any).isComponentValid(mockChargingStation, component)
+      expect(isValid).toBe(false)
+    })
+  })
+
+  await describe('Variable support validation tests', async () => {
+    const manager = OCPP20VariableManager.getInstance()
+
+    await it('Should support standard HeartbeatInterval variable', () => {
+      const component: ComponentType = { name: OCPP20ComponentName.ChargingStation }
+      const variable: VariableType = { name: OCPP20OptionalVariableName.HeartbeatInterval }
+
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+      const isSupported = (manager as any).isVariableSupported(
+        mockChargingStation,
+        component,
+        variable
+      )
+      expect(isSupported).toBe(true)
+    })
+
+    await it('Should support known OCPP variables', () => {
+      const component: ComponentType = { name: OCPP20ComponentName.ChargingStation }
+      const variable: VariableType = { name: OCPP20OptionalVariableName.WebSocketPingInterval }
+
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+      const isSupported = (manager as any).isVariableSupported(
+        mockChargingStation,
+        component,
+        variable
+      )
+      expect(isSupported).toBe(true)
+    })
+
+    await it('Should reject unknown variables', () => {
+      const component: ComponentType = { name: OCPP20ComponentName.ChargingStation }
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
+      const variable: VariableType = { name: 'UnknownVariable' as any }
+
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+      const isSupported = (manager as any).isVariableSupported(
+        mockChargingStation,
+        component,
+        variable
+      )
+      expect(isSupported).toBe(false)
+    })
+  })
+
+  await describe('Attribute type validation tests', async () => {
+    const manager = OCPP20VariableManager.getInstance()
+
+    await it('Should support Actual attribute by default', () => {
+      const variable: VariableType = { name: OCPP20OptionalVariableName.HeartbeatInterval }
+
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+      const isSupported = (manager as any).isAttributeTypeSupported(
+        variable,
+        AttributeEnumType.Actual
+      )
+      expect(isSupported).toBe(true)
+    })
+
+    await it('Should reject unsupported attribute types for most variables', () => {
+      const variable: VariableType = { name: OCPP20OptionalVariableName.HeartbeatInterval }
+
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+      const isSupported = (manager as any).isAttributeTypeSupported(
+        variable,
+        AttributeEnumType.Target
+      )
+      expect(isSupported).toBe(false)
+    })
+  })
+})