]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
test(ocpp2.0): add BootNotification response handler tests
authorJérôme Benoit <jerome.benoit@sap.com>
Tue, 3 Mar 2026 18:09:21 +0000 (19:09 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Tue, 3 Mar 2026 18:09:21 +0000 (19:09 +0100)
Test all 6 branches of handleResponseBootNotification:
ACCEPTED/PENDING/REJECTED status, interval handling,
missing interval, and invalid status.

tests/charging-station/ocpp/2.0/OCPP20ResponseService-BootNotification.test.ts [new file with mode: 0644]

diff --git a/tests/charging-station/ocpp/2.0/OCPP20ResponseService-BootNotification.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ResponseService-BootNotification.test.ts
new file mode 100644 (file)
index 0000000..bfbd9fa
--- /dev/null
@@ -0,0 +1,156 @@
+/**
+ * @file Tests for OCPP20ResponseService BootNotification response handler
+ * @description Verifies correct handling of BootNotification responses
+ *
+ * Covers:
+ * - B01 BootNotificationResponse handler branch coverage
+ * - ACCEPTED status — stores response and emits accepted event
+ * - PENDING status — stores response and emits pending event
+ * - REJECTED status — stores response and emits rejected event
+ * - HeartbeatInterval configuration key set when interval provided
+ * - Interval handling skipped when interval absent
+ * - Invalid registration status — deletes response and logs error
+ */
+
+import { expect } from '@std/expect'
+import { afterEach, beforeEach, describe, it, mock } from 'node:test'
+
+import type { MockChargingStation } from '../../ChargingStationTestUtils.js'
+
+import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js'
+import {
+  ChargingStationEvents,
+  type OCPP20BootNotificationResponse,
+  OCPP20OptionalVariableName,
+  OCPP20RequestCommand,
+  OCPPVersion,
+  RegistrationStatusEnumType,
+} from '../../../../src/types/index.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
+import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
+
+/**
+ * Create a mock station suitable for BootNotification response tests.
+ * Uses ocppStrictCompliance: false to bypass AJV validation so the
+ * handler logic can be tested in isolation.
+ * @returns A mock station configured for BootNotification tests
+ */
+function createBootNotificationStation (): MockChargingStation {
+  const { station } = createMockChargingStation({
+    baseName: TEST_CHARGING_STATION_BASE_NAME,
+    connectorsCount: 1,
+    heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+    stationInfo: {
+      // Bypass AJV schema validation — tests focus on handler logic
+      ocppStrictCompliance: false,
+      ocppVersion: OCPPVersion.VERSION_201,
+    },
+    websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+  })
+  return station as MockChargingStation
+}
+
+await describe('B01 - BootNotificationResponse handler', async () => {
+  let responseService: OCPP20ResponseService
+  let mockStation: MockChargingStation
+
+  beforeEach(() => {
+    mock.timers.enable({ apis: ['setInterval', 'setTimeout'] })
+    responseService = new OCPP20ResponseService()
+    mockStation = createBootNotificationStation()
+  })
+
+  afterEach(() => {
+    standardCleanup()
+  })
+
+  /**
+   * Helper to dispatch a BootNotificationResponse through the public responseHandler.
+   * @param payload - The BootNotificationResponse payload to dispatch
+   * @returns Resolves when the response handler completes
+   */
+  async function dispatch (payload: OCPP20BootNotificationResponse): Promise<void> {
+    await responseService.responseHandler(
+      mockStation,
+      OCPP20RequestCommand.BOOT_NOTIFICATION,
+      payload as unknown as Parameters<typeof responseService.responseHandler>[2],
+      {} as Parameters<typeof responseService.responseHandler>[3]
+    )
+  }
+
+  await it('should store response and emit accepted event for ACCEPTED status', async () => {
+    const emitSpy = mock.method(mockStation, 'emitChargingStationEvent')
+    const payload: OCPP20BootNotificationResponse = {
+      currentTime: new Date(),
+      interval: 300,
+      status: RegistrationStatusEnumType.ACCEPTED,
+    }
+    await dispatch(payload)
+    expect(mockStation.bootNotificationResponse).toBe(payload)
+    expect(emitSpy.mock.calls.length).toBe(1)
+    expect(emitSpy.mock.calls[0].arguments[0]).toBe(ChargingStationEvents.accepted)
+  })
+
+  await it('should store response and emit pending event for PENDING status', async () => {
+    const emitSpy = mock.method(mockStation, 'emitChargingStationEvent')
+    const payload: OCPP20BootNotificationResponse = {
+      currentTime: new Date(),
+      interval: 300,
+      status: RegistrationStatusEnumType.PENDING,
+    }
+    await dispatch(payload)
+    expect(mockStation.bootNotificationResponse).toBe(payload)
+    expect(emitSpy.mock.calls.length).toBe(1)
+    expect(emitSpy.mock.calls[0].arguments[0]).toBe(ChargingStationEvents.pending)
+  })
+
+  await it('should store response and emit rejected event for REJECTED status', async () => {
+    const emitSpy = mock.method(mockStation, 'emitChargingStationEvent')
+    const payload: OCPP20BootNotificationResponse = {
+      currentTime: new Date(),
+      interval: 300,
+      status: RegistrationStatusEnumType.REJECTED,
+    }
+    await dispatch(payload)
+    expect(mockStation.bootNotificationResponse).toBe(payload)
+    expect(emitSpy.mock.calls.length).toBe(1)
+    expect(emitSpy.mock.calls[0].arguments[0]).toBe(ChargingStationEvents.rejected)
+  })
+
+  await it('should set HeartbeatInterval configuration key when interval is provided', async () => {
+    const payload: OCPP20BootNotificationResponse = {
+      currentTime: new Date(),
+      interval: 300,
+      status: RegistrationStatusEnumType.ACCEPTED,
+    }
+    await dispatch(payload)
+    const configKey = mockStation.ocppConfiguration?.configurationKey
+    expect(configKey).toBeDefined()
+    expect(configKey?.length).toBe(1)
+    expect(configKey?.[0]?.key).toBe(OCPP20OptionalVariableName.HeartbeatInterval)
+    expect(configKey?.[0]?.value).toBe('300')
+  })
+
+  await it('should skip interval handling when interval is not provided', async () => {
+    const payload = {
+      currentTime: new Date(),
+      status: RegistrationStatusEnumType.ACCEPTED,
+    } as unknown as OCPP20BootNotificationResponse
+    await dispatch(payload)
+    const configKey = mockStation.ocppConfiguration?.configurationKey
+    expect(configKey).toBeDefined()
+    expect(configKey?.length).toBe(0)
+  })
+
+  await it('should delete response and log error for invalid registration status', async () => {
+    const payload = {
+      currentTime: new Date(),
+      interval: 300,
+      status: 'INVALID_STATUS',
+    } as unknown as OCPP20BootNotificationResponse
+    await dispatch(payload)
+    expect(mockStation.bootNotificationResponse).toBeUndefined()
+  })
+})