]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
test(ocpp): strengthen mock call verification in OCPP 2.0 handler tests
authorJérôme Benoit <jerome.benoit@sap.com>
Tue, 31 Mar 2026 19:26:36 +0000 (21:26 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Tue, 31 Mar 2026 19:26:36 +0000 (21:26 +0200)
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

tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-CertificateSigned.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ChangeAvailability.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ClearCache.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-UnlockConnector.test.ts

index db07aab659dd275996a0a4e758370134d02d086e..cde145f4f10ff799c3debdc3ea217c125263c9d3 100644 (file)
@@ -41,6 +41,7 @@ import {
 await describe('I04 - CertificateSigned', async () => {
   let station: ChargingStation
   let stationWithCertManager: ChargingStationWithCertificateManager
+  let closeWSConnectionMock: ReturnType<typeof mock.fn>
   let testableService: ReturnType<typeof createTestableIncomingRequestService>
 
   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)
     })
   })
 
index 190ee6f0a330ad4b2191125b245cf9a1a309f00c..2b885e2b2932b18f32f99258653f0f17f44e938f 100644 (file)
@@ -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<typeof mock.fn>
   let testableService: ReturnType<typeof createTestableIncomingRequestService>
 
   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)', () => {
index 5a30056d3d8a157b0349b391689e8df5eb0dea6e..3b612a31cb8e983a604889f3f49fd6f4b4699c8a 100644 (file)
@@ -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 })
index f4d3b3e9ef7d7799f261fb97f4e923c364b39554..f31dc83aab80095049d5da895d9e3d7b0a2e48f6 100644 (file)
@@ -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 () => {