From ca3eec7dc4656fb2bdd002c8c088aeb167caf2c4 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Tue, 31 Mar 2026 21:26:36 +0200 Subject: [PATCH] test(ocpp): strengthen mock call verification in OCPP 2.0 handler tests Replace loose assertions (assert.ok(>0), boolean flags) with strict mock.fn() callCount() and argument verification across 4 test files: - UnlockConnector: verify exact call count + STATUS_NOTIFICATION command - ClearCache: replace 3 boolean flags with mock.fn() + callCount() - CertificateSigned: verify closeWSConnection called/not-called per cert type - ChangeAvailability: capture requestHandler via factory, verify side effects --- ...ngRequestService-CertificateSigned.test.ts | 16 ++++---- ...gRequestService-ChangeAvailability.test.ts | 37 +++++++++---------- ...0IncomingRequestService-ClearCache.test.ts | 26 +++++-------- ...mingRequestService-UnlockConnector.test.ts | 5 ++- 4 files changed, 40 insertions(+), 44 deletions(-) diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-CertificateSigned.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-CertificateSigned.test.ts index db07aab6..cde145f4 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-CertificateSigned.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-CertificateSigned.test.ts @@ -41,6 +41,7 @@ import { await describe('I04 - CertificateSigned', async () => { let station: ChargingStation let stationWithCertManager: ChargingStationWithCertificateManager + let closeWSConnectionMock: ReturnType let testableService: ReturnType beforeEach(() => { @@ -60,7 +61,8 @@ await describe('I04 - CertificateSigned', async () => { station, createMockCertificateManager() ) - station.closeWSConnection = mock.fn() + closeWSConnectionMock = mock.fn() + station.closeWSConnection = closeWSConnectionMock as unknown as () => void testableService = createTestableIncomingRequestService(new OCPP20IncomingRequestService()) }) @@ -86,6 +88,7 @@ await describe('I04 - CertificateSigned', async () => { assert.notStrictEqual(response.status, undefined) assert.strictEqual(typeof response.status, 'string') assert.strictEqual(response.status, GenericStatus.Accepted) + assert.strictEqual(closeWSConnectionMock.mock.callCount(), 1) }) await it('should accept single certificate (no chain)', async () => { @@ -104,6 +107,7 @@ await describe('I04 - CertificateSigned', async () => { assert.notStrictEqual(response, undefined) assert.strictEqual(response.status, GenericStatus.Accepted) assert.strictEqual(response.statusInfo, undefined) + assert.strictEqual(closeWSConnectionMock.mock.callCount(), 0) }) }) @@ -122,6 +126,7 @@ await describe('I04 - CertificateSigned', async () => { assert.notStrictEqual(response.statusInfo, undefined) assert.notStrictEqual(response.statusInfo?.reasonCode, undefined) assert.strictEqual(typeof response.statusInfo?.reasonCode, 'string') + assert.strictEqual(closeWSConnectionMock.mock.callCount(), 0) }) }) @@ -143,8 +148,7 @@ await describe('I04 - CertificateSigned', async () => { await testableService.handleRequestCertificateSigned(station, request) assert.strictEqual(response.status, GenericStatus.Accepted) - // Verify closeWSConnection was called to trigger reconnect - assert.ok(mockCloseWSConnection.mock.calls.length > 0) + assert.strictEqual(mockCloseWSConnection.mock.callCount(), 1) }) }) @@ -166,10 +170,8 @@ await describe('I04 - CertificateSigned', async () => { await testableService.handleRequestCertificateSigned(station, request) assert.strictEqual(response.status, GenericStatus.Accepted) - // Verify storeCertificate was called - assert.ok(mockCertManager.storeCertificate.mock.calls.length > 0) - // Verify closeWSConnection was NOT called for V2GCertificate - assert.strictEqual(mockCloseWSConnection.mock.calls.length, 0) + assert.strictEqual(mockCertManager.storeCertificate.mock.callCount(), 1) + assert.strictEqual(mockCloseWSConnection.mock.callCount(), 0) }) }) diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ChangeAvailability.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ChangeAvailability.test.ts index 190ee6f0..2b885e2b 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ChangeAvailability.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ChangeAvailability.test.ts @@ -3,6 +3,8 @@ * @description Unit tests for OCPP 2.0.1 ChangeAvailability command handling (G03) */ +import type { mock } from 'node:test' + import assert from 'node:assert/strict' import { afterEach, beforeEach, describe, it } from 'node:test' @@ -13,37 +15,26 @@ import { OCPP20IncomingRequestService } from '../../../../src/charging-station/o import { ChangeAvailabilityStatusEnumType, OCPP20OperationalStatusEnumType, - OCPPVersion, + OCPP20RequestCommand, ReasonCodeEnumType, } from '../../../../src/types/index.js' -import { Constants } from '../../../../src/utils/index.js' import { + flushMicrotasks, setupConnectorWithTransaction, standardCleanup, } from '../../../helpers/TestLifecycleHelpers.js' import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js' -import { createMockChargingStation } from '../../ChargingStationTestUtils.js' +import { createOCPP20ListenerStation } from './OCPP20TestUtils.js' await describe('G03 - ChangeAvailability', async () => { let station: ChargingStation + let requestHandlerMock: ReturnType let testableService: ReturnType beforeEach(() => { - const { station: mockStation } = createMockChargingStation({ - baseName: TEST_CHARGING_STATION_BASE_NAME, - connectorsCount: 3, - evseConfiguration: { evsesCount: 3 }, - heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL, - ocppRequestService: { - requestHandler: async () => await Promise.resolve({}), - }, - stationInfo: { - ocppStrictCompliance: false, - ocppVersion: OCPPVersion.VERSION_201, - }, - websocketPingInterval: Constants.DEFAULT_WS_PING_INTERVAL, - }) - station = mockStation + ;({ requestHandlerMock, station } = createOCPP20ListenerStation( + TEST_CHARGING_STATION_BASE_NAME + )) const incomingRequestService = new OCPP20IncomingRequestService() testableService = createTestableIncomingRequestService(incomingRequestService) }) @@ -53,7 +44,7 @@ await describe('G03 - ChangeAvailability', async () => { }) // FR: G03.FR.01 - await it('should accept EVSE-level Inoperative when no ongoing transaction', () => { + await it('should accept EVSE-level Inoperative when no ongoing transaction', async () => { const response = testableService.handleRequestChangeAvailability(station, { evse: { id: 1 }, operationalStatus: OCPP20OperationalStatusEnumType.Inoperative, @@ -62,6 +53,10 @@ await describe('G03 - ChangeAvailability', async () => { assert.strictEqual(response.status, ChangeAvailabilityStatusEnumType.Accepted) const evseStatus = station.getEvseStatus(1) assert.strictEqual(evseStatus?.availability, OCPP20OperationalStatusEnumType.Inoperative) + await flushMicrotasks() + assert.ok(requestHandlerMock.mock.callCount() >= 1) + const args = requestHandlerMock.mock.calls[0].arguments as [unknown, string] + assert.strictEqual(args[1], OCPP20RequestCommand.STATUS_NOTIFICATION) }) // FR: G03.FR.02 @@ -107,7 +102,7 @@ await describe('G03 - ChangeAvailability', async () => { assert.strictEqual(response.status, ChangeAvailabilityStatusEnumType.Scheduled) }) - await it('should reject when EVSE does not exist', () => { + await it('should reject when EVSE does not exist', async () => { const response = testableService.handleRequestChangeAvailability(station, { evse: { id: 999 }, operationalStatus: OCPP20OperationalStatusEnumType.Inoperative, @@ -116,6 +111,8 @@ await describe('G03 - ChangeAvailability', async () => { assert.strictEqual(response.status, ChangeAvailabilityStatusEnumType.Rejected) assert.notStrictEqual(response.statusInfo, undefined) assert.strictEqual(response.statusInfo?.reasonCode, ReasonCodeEnumType.UnknownEvse) + await flushMicrotasks() + assert.strictEqual(requestHandlerMock.mock.callCount(), 0) }) await it('should accept when already in requested state (idempotent)', () => { diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ClearCache.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ClearCache.test.ts index 5a30056d..3b612a31 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ClearCache.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ClearCache.test.ts @@ -4,7 +4,7 @@ */ import assert from 'node:assert/strict' -import { afterEach, beforeEach, describe, it } from 'node:test' +import { afterEach, beforeEach, describe, it, mock } from 'node:test' import type { ChargingStation } from '../../../../src/charging-station/index.js' @@ -68,11 +68,9 @@ await describe('C11 - Clear Authorization Data in Authorization Cache', async () await describe('CLR-001 - ClearCache clears Authorization Cache', async () => { await it('should call authService.clearCache() on ClearCache request', async () => { // Create a mock auth service to verify clearCache is called - let clearCacheCalled = false + const clearCacheMock = mock.fn() const mockAuthService = { - clearCache: (): void => { - clearCacheCalled = true - }, + clearCache: clearCacheMock, getConfiguration: () => ({ authorizationCacheEnabled: true, }), @@ -87,7 +85,7 @@ await describe('C11 - Clear Authorization Data in Authorization Cache', async () try { const response = await testableService.handleRequestClearCache(station) - assert.strictEqual(clearCacheCalled, true) + assert.strictEqual(clearCacheMock.mock.callCount(), 1) assert.strictEqual(response.status, GenericStatus.Accepted) } finally { // Restore original factory method @@ -97,18 +95,16 @@ await describe('C11 - Clear Authorization Data in Authorization Cache', async () await it('should not call idTagsCache.deleteIdTags() on ClearCache request', async () => { // Verify that IdTagsCache is not touched - let deleteIdTagsCalled = false + const deleteIdTagsMock = mock.fn() const originalDeleteIdTags = station.idTagsCache.deleteIdTags.bind(station.idTagsCache) Object.assign(station.idTagsCache, { - deleteIdTags: () => { - deleteIdTagsCalled = true - }, + deleteIdTags: deleteIdTagsMock, }) try { await testableService.handleRequestClearCache(station) - assert.strictEqual(deleteIdTagsCalled, false) + assert.strictEqual(deleteIdTagsMock.mock.callCount(), 0) } finally { // Restore original method Object.assign(station.idTagsCache, { deleteIdTags: originalDeleteIdTags }) @@ -200,11 +196,9 @@ await describe('C11 - Clear Authorization Data in Authorization Cache', async () }) await it('should not attempt to clear cache when AuthCacheEnabled is false', async () => { - let clearCacheAttempted = false + const clearCacheMock = mock.fn() const mockAuthService = { - clearCache: (): void => { - clearCacheAttempted = true - }, + clearCache: clearCacheMock, getConfiguration: () => ({ authorizationCacheEnabled: false, }), @@ -220,7 +214,7 @@ await describe('C11 - Clear Authorization Data in Authorization Cache', async () await testableService.handleRequestClearCache(station) // clearCache should NOT be called when cache is disabled - assert.strictEqual(clearCacheAttempted, false) + assert.strictEqual(clearCacheMock.mock.callCount(), 0) } finally { // Restore original factory method Object.assign(OCPPAuthServiceFactory, { getInstance: originalGetInstance }) diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-UnlockConnector.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-UnlockConnector.test.ts index f4d3b3e9..f31dc83a 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-UnlockConnector.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-UnlockConnector.test.ts @@ -15,6 +15,7 @@ import type { MockChargingStation } from '../../ChargingStationTestUtils.js' import { createTestableIncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js' import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.js' import { + OCPP20RequestCommand, OCPPVersion, ReasonCodeEnumType, UnlockStatusEnumType, @@ -206,7 +207,9 @@ await describe('F05 - UnlockConnector', async () => { await testableService.handleRequestUnlockConnector(mockStation, request) // sendAndSetConnectorStatus calls requestHandler internally for StatusNotification - assert.ok(requestHandlerMock.mock.calls.length > 0) + assert.strictEqual(requestHandlerMock.mock.callCount(), 1) + const args = requestHandlerMock.mock.calls[0].arguments as [unknown, string] + assert.strictEqual(args[1], OCPP20RequestCommand.STATUS_NOTIFICATION) }) await it('should return a Promise from async handler', async () => { -- 2.43.0