From 07ae446e1f077b27847b0ca8f419e7f2af3cd4fe Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Tue, 3 Mar 2026 19:09:21 +0100 Subject: [PATCH] test(ocpp2.0): add BootNotification response handler tests Test all 6 branches of handleResponseBootNotification: ACCEPTED/PENDING/REJECTED status, interval handling, missing interval, and invalid status. --- ...20ResponseService-BootNotification.test.ts | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 tests/charging-station/ocpp/2.0/OCPP20ResponseService-BootNotification.test.ts 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 index 00000000..bfbd9fac --- /dev/null +++ b/tests/charging-station/ocpp/2.0/OCPP20ResponseService-BootNotification.test.ts @@ -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 { + await responseService.responseHandler( + mockStation, + OCPP20RequestCommand.BOOT_NOTIFICATION, + payload as unknown as Parameters[2], + {} as Parameters[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() + }) +}) -- 2.43.0