]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(tests): add shared test helpers and fixtures for DRY compliance
authorJérôme Benoit <jerome.benoit@sap.com>
Sat, 28 Feb 2026 14:31:06 +0000 (15:31 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Sat, 28 Feb 2026 14:34:49 +0000 (15:34 +0100)
- Add timer helpers (withMockTimers, createTimerScope) in TestLifecycleHelpers.ts
- Add auth mock factories (createTestAuthConfig, createMockAuthCache, etc.) in MockFactories.ts
- Add transaction fixtures (IdTokenFixtures, TransactionContextFixtures) in OCPP20TestUtils.ts
- Add request tracking helper (createMockStationWithRequestTracking) for OCPP tests
- Refactor AuthStrategy tests to use shared factories (~490 LOC saved)
- Refactor TransactionEvent tests to use shared fixtures and helpers

14 files changed, 585 insertions(+), 752 deletions(-) = 167 net LOC saved

14 files changed:
tests/charging-station/ChargingStation-Configuration.test.ts
tests/charging-station/ChargingStation-Transactions.test.ts
tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-CableFirst.test.ts
tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-IdTokenFirst.test.ts
tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-Offline.test.ts
tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-Periodic.test.ts
tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts
tests/charging-station/ocpp/auth/helpers/MockFactories.ts
tests/charging-station/ocpp/auth/strategies/CertificateAuthStrategy.test.ts
tests/charging-station/ocpp/auth/strategies/LocalAuthStrategy.test.ts
tests/charging-station/ocpp/auth/strategies/RemoteAuthStrategy.test.ts
tests/helpers/TestLifecycleHelpers.ts
tests/utils/Utils.test.ts
tests/worker/WorkerUtils.test.ts

index 66d2ba790d39a52bb3f5617730457f3a6c8cd7e0..09ff83cac36eb40374eef3997a1b0d0f673f85e6 100644 (file)
@@ -8,6 +8,7 @@ import { afterEach, describe, it } from 'node:test'
 import type { ChargingStation } from '../../src/charging-station/ChargingStation.js'
 
 import { AvailabilityType, RegistrationStatusEnumType } from '../../src/types/index.js'
+import { withMockTimers } from '../helpers/TestLifecycleHelpers.js'
 import { cleanupChargingStation, createMockChargingStation } from './ChargingStationTestUtils.js'
 
 // Alias for tests that reference createRealChargingStation
@@ -739,9 +740,8 @@ await describe('ChargingStation Configuration Management', async () => {
       }
     })
 
-    await it('should return valid WebSocket ping interval from getWebSocketPingInterval()', t => {
-      t.mock.timers.enable({ apis: ['setInterval'] })
-      try {
+    await it('should return valid WebSocket ping interval from getWebSocketPingInterval()', async t => {
+      await withMockTimers(t, ['setInterval'], () => {
         // Arrange
         const result = createMockChargingStation({ connectorsCount: 1 })
         station = result.station
@@ -752,14 +752,11 @@ await describe('ChargingStation Configuration Management', async () => {
         // Assert - should return a valid interval value
         expect(pingInterval).toBeGreaterThanOrEqual(0)
         expect(typeof pingInterval).toBe('number')
-      } finally {
-        t.mock.timers.reset()
-      }
+      })
     })
 
-    await it('should restart WebSocket ping when restartWebSocketPing() is called', t => {
-      t.mock.timers.enable({ apis: ['setInterval'] })
-      try {
+    await it('should restart WebSocket ping when restartWebSocketPing() is called', async t => {
+      await withMockTimers(t, ['setInterval'], () => {
         // Arrange
         const result = createMockChargingStation({ connectorsCount: 1 })
         station = result.station
@@ -769,9 +766,7 @@ await describe('ChargingStation Configuration Management', async () => {
 
         // Assert - should complete without error
         expect(station).toBeDefined()
-      } finally {
-        t.mock.timers.reset()
-      }
+      })
     })
   })
 })
index 2035c3ac3a27fb19cf7b206ccc8364d0c3643cf9..b125c8a8a8784c238bc6d71800d6db3e04e7f269 100644 (file)
@@ -7,6 +7,7 @@ import { afterEach, describe, it } from 'node:test'
 
 import type { ChargingStation } from '../../src/charging-station/ChargingStation.js'
 
+import { withMockTimers } from '../helpers/TestLifecycleHelpers.js'
 import { cleanupChargingStation, createMockChargingStation } from './ChargingStationTestUtils.js'
 
 await describe('ChargingStation Transaction Management', async () => {
@@ -433,9 +434,8 @@ await describe('ChargingStation Transaction Management', async () => {
       }
     })
 
-    await it('should create interval when startHeartbeat() is called with valid interval', t => {
-      t.mock.timers.enable({ apis: ['setInterval'] })
-      try {
+    await it('should create interval when startHeartbeat() is called with valid interval', async t => {
+      await withMockTimers(t, ['setInterval'], () => {
         // Arrange
         const result = createMockChargingStation({ connectorsCount: 1, heartbeatInterval: 30000 })
         station = result.station
@@ -446,14 +446,11 @@ await describe('ChargingStation Transaction Management', async () => {
         // Assert - heartbeat interval should be created
         expect(station.heartbeatSetInterval).toBeDefined()
         expect(typeof station.heartbeatSetInterval).toBe('object')
-      } finally {
-        t.mock.timers.reset()
-      }
+      })
     })
 
-    await it('should restart heartbeat interval when restartHeartbeat() is called', t => {
-      t.mock.timers.enable({ apis: ['setInterval'] })
-      try {
+    await it('should restart heartbeat interval when restartHeartbeat() is called', async t => {
+      await withMockTimers(t, ['setInterval'], () => {
         // Arrange
         const result = createMockChargingStation({ connectorsCount: 1, heartbeatInterval: 30000 })
         station = result.station
@@ -468,14 +465,11 @@ await describe('ChargingStation Transaction Management', async () => {
         expect(secondInterval).toBeDefined()
         expect(typeof secondInterval).toBe('object')
         expect(firstInterval !== secondInterval).toBe(true)
-      } finally {
-        t.mock.timers.reset()
-      }
+      })
     })
 
-    await it('should not create heartbeat interval if already started', t => {
-      t.mock.timers.enable({ apis: ['setInterval'] })
-      try {
+    await it('should not create heartbeat interval if already started', async t => {
+      await withMockTimers(t, ['setInterval'], () => {
         // Arrange
         const result = createMockChargingStation({ connectorsCount: 1, heartbeatInterval: 30000 })
         station = result.station
@@ -488,14 +482,11 @@ await describe('ChargingStation Transaction Management', async () => {
 
         // Assert - interval should be same (not restarted)
         expect(firstInterval).toBe(secondInterval)
-      } finally {
-        t.mock.timers.reset()
-      }
+      })
     })
 
-    await it('should create meter values interval when startMeterValues() is called for active transaction', t => {
-      t.mock.timers.enable({ apis: ['setInterval'] })
-      try {
+    await it('should create meter values interval when startMeterValues() is called for active transaction', async t => {
+      await withMockTimers(t, ['setInterval'], () => {
         // Arrange
         const result = createMockChargingStation({ connectorsCount: 2 })
         station = result.station
@@ -513,14 +504,11 @@ await describe('ChargingStation Transaction Management', async () => {
           expect(connector1.transactionSetInterval).toBeDefined()
           expect(typeof connector1.transactionSetInterval).toBe('object')
         }
-      } finally {
-        t.mock.timers.reset()
-      }
+      })
     })
 
-    await it('should restart meter values interval when restartMeterValues() is called', t => {
-      t.mock.timers.enable({ apis: ['setInterval'] })
-      try {
+    await it('should restart meter values interval when restartMeterValues() is called', async t => {
+      await withMockTimers(t, ['setInterval'], () => {
         // Arrange
         const result = createMockChargingStation({ connectorsCount: 2 })
         station = result.station
@@ -540,14 +528,11 @@ await describe('ChargingStation Transaction Management', async () => {
         expect(secondInterval).toBeDefined()
         expect(typeof secondInterval).toBe('object')
         expect(firstInterval !== secondInterval).toBe(true)
-      } finally {
-        t.mock.timers.reset()
-      }
+      })
     })
 
-    await it('should clear meter values interval when stopMeterValues() is called', t => {
-      t.mock.timers.enable({ apis: ['setInterval'] })
-      try {
+    await it('should clear meter values interval when stopMeterValues() is called', async t => {
+      await withMockTimers(t, ['setInterval'], () => {
         // Arrange
         const result = createMockChargingStation({ connectorsCount: 2 })
         station = result.station
@@ -563,14 +548,11 @@ await describe('ChargingStation Transaction Management', async () => {
 
         // Assert - interval should be cleared
         expect(connector1?.transactionSetInterval).toBeUndefined()
-      } finally {
-        t.mock.timers.reset()
-      }
+      })
     })
 
-    await it('should create transaction updated interval when startTxUpdatedInterval() is called for OCPP 2.0', t => {
-      t.mock.timers.enable({ apis: ['setInterval'] })
-      try {
+    await it('should create transaction updated interval when startTxUpdatedInterval() is called for OCPP 2.0', async t => {
+      await withMockTimers(t, ['setInterval'], () => {
         // Arrange
         const result = createMockChargingStation({ connectorsCount: 2, ocppVersion: '2.0' })
         station = result.station
@@ -588,14 +570,11 @@ await describe('ChargingStation Transaction Management', async () => {
           expect(connector1.transactionTxUpdatedSetInterval).toBeDefined()
           expect(typeof connector1.transactionTxUpdatedSetInterval).toBe('object')
         }
-      } finally {
-        t.mock.timers.reset()
-      }
+      })
     })
 
-    await it('should clear transaction updated interval when stopTxUpdatedInterval() is called', t => {
-      t.mock.timers.enable({ apis: ['setInterval'] })
-      try {
+    await it('should clear transaction updated interval when stopTxUpdatedInterval() is called', async t => {
+      await withMockTimers(t, ['setInterval'], () => {
         // Arrange
         const result = createMockChargingStation({ connectorsCount: 2, ocppVersion: '2.0' })
         station = result.station
@@ -611,9 +590,7 @@ await describe('ChargingStation Transaction Management', async () => {
 
         // Assert - interval should be cleared
         expect(connector1?.transactionTxUpdatedSetInterval).toBeUndefined()
-      } finally {
-        t.mock.timers.reset()
-      }
+      })
     })
   })
 })
index 17c6048e937adc9eccdf8635634e59aa9ae93e69..7c8300f5850e80044fcd17c74caad1ce4cfdb8a0 100644 (file)
@@ -11,15 +11,13 @@ import {
   OCPP20TransactionEventEnumType,
   OCPP20TriggerReasonEnumType,
 } from '../../../../src/types/index.js'
-import {
-  OCPP20ChargingStateEnumType,
-  type OCPP20TransactionContext,
-} from '../../../../src/types/ocpp/2.0/Transaction.js'
+import { OCPP20ChargingStateEnumType } from '../../../../src/types/ocpp/2.0/Transaction.js'
 import { generateUUID } from '../../../../src/utils/index.js'
 import {
   createMockOCPP20TransactionTestStation,
   resetConnectorTransactionState,
   resetLimits,
+  TransactionContextFixtures,
 } from './OCPP20TestUtils.js'
 
 /**
@@ -412,42 +410,27 @@ await describe('E02 - Cable-First Transaction Flow', async () => {
   // =========================================================================
   await describe('Context-Based Cable Event Trigger Selection', async () => {
     await it('should select CablePluggedIn from cable_action context with plugged_in state', () => {
-      const context: OCPP20TransactionContext = {
-        cableState: 'plugged_in',
-        source: 'cable_action',
-      }
-
       const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
         OCPP20TransactionEventEnumType.Started,
-        context
+        TransactionContextFixtures.cablePluggedIn()
       )
 
       expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn)
     })
 
     await it('should select EVDetected from cable_action context with detected state', () => {
-      const context: OCPP20TransactionContext = {
-        cableState: 'detected',
-        source: 'cable_action',
-      }
-
       const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
         OCPP20TransactionEventEnumType.Updated,
-        context
+        TransactionContextFixtures.evDetected()
       )
 
       expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.EVDetected)
     })
 
     await it('should select EVDeparted from cable_action context with unplugged state', () => {
-      const context: OCPP20TransactionContext = {
-        cableState: 'unplugged',
-        source: 'cable_action',
-      }
-
       const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
         OCPP20TransactionEventEnumType.Ended,
-        context
+        TransactionContextFixtures.evDeparted()
       )
 
       expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.EVDeparted)
index 3212b638387d0662ec1fdab9316b5d28a9e8e7d1..9830cc02892124269e0b295f5538f3201b94ef82 100644 (file)
@@ -21,6 +21,7 @@ import {
   createMockOCPP20TransactionTestStation,
   resetConnectorTransactionState,
   resetLimits,
+  TransactionContextFixtures,
 } from './OCPP20TestUtils.js'
 
 /**
@@ -56,14 +57,9 @@ await describe('E03 - IdToken-First Pre-Authorization Flow', async () => {
   await describe('E03.FR.13 - Trigger Reason Selection', async () => {
     await it('should select Authorized trigger for IdToken-first transaction start', () => {
       // E03.FR.13: triggerReason SHALL be Authorized for IdToken-first
-      const context: OCPP20TransactionContext = {
-        authorizationMethod: 'idToken',
-        source: 'local_authorization',
-      }
-
       const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
         OCPP20TransactionEventEnumType.Started,
-        context
+        TransactionContextFixtures.idTokenAuthorized()
       )
 
       expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized)
index 653f7d6b78a661e1e9affd9e9ebdc2178100cb6c..250815d384d8c1e69b8fef8e2add0fdbba78b278 100644 (file)
@@ -19,46 +19,23 @@ import { Constants, generateUUID } from '../../../../src/utils/index.js'
 import { standardCleanup } from '../../../../tests/helpers/TestLifecycleHelpers.js'
 import { createChargingStation } from '../../../ChargingStationFactory.js'
 import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
-import { resetLimits } from './OCPP20TestUtils.js'
+import {
+  type CapturedOCPPRequest,
+  createMockStationWithRequestTracking,
+  type MockStationWithTracking,
+} from './OCPP20TestUtils.js'
 
 await describe('E02 - OCPP 2.0.1 Offline TransactionEvent Queueing', async () => {
+  let mockTracking: MockStationWithTracking
   let mockChargingStation: ChargingStation
-  let requestHandlerMock: ReturnType<typeof mock.fn>
-  interface SentRequest {
-    command: string
-    payload: Record<string, unknown>
-  }
-  let sentRequests: SentRequest[]
-  let isOnline: boolean
+  let sentRequests: CapturedOCPPRequest[]
+  let setOnline: (online: boolean) => void
 
   beforeEach(() => {
-    sentRequests = []
-    isOnline = true
-    requestHandlerMock = mock.fn(
-      async (_station: ChargingStation, command: string, payload: Record<string, unknown>) => {
-        sentRequests.push({ command, payload })
-        return Promise.resolve({} as EmptyObject)
-      }
-    )
-
-    mockChargingStation = createChargingStation({
-      baseName: TEST_CHARGING_STATION_BASE_NAME,
-      connectorsCount: 3,
-      evseConfiguration: { evsesCount: 3 },
-      heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
-      ocppRequestService: {
-        requestHandler: requestHandlerMock,
-      },
-      stationInfo: {
-        ocppStrictCompliance: true,
-        ocppVersion: OCPPVersion.VERSION_201,
-      },
-      websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
-    })
-
-    mockChargingStation.isWebSocketConnectionOpened = () => isOnline
-
-    resetLimits(mockChargingStation)
+    mockTracking = createMockStationWithRequestTracking()
+    mockChargingStation = mockTracking.station
+    sentRequests = mockTracking.sentRequests
+    setOnline = mockTracking.setOnline
   })
 
   afterEach(() => {
@@ -76,7 +53,7 @@ await describe('E02 - OCPP 2.0.1 Offline TransactionEvent Queueing', async () =>
       const connectorId = 1
       const transactionId = generateUUID()
 
-      isOnline = false
+      setOnline(false)
 
       OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
@@ -102,7 +79,7 @@ await describe('E02 - OCPP 2.0.1 Offline TransactionEvent Queueing', async () =>
       const connectorId = 1
       const transactionId = generateUUID()
 
-      isOnline = false
+      setOnline(false)
 
       OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
@@ -152,7 +129,7 @@ await describe('E02 - OCPP 2.0.1 Offline TransactionEvent Queueing', async () =>
       const connectorId = 1
       const transactionId = generateUUID()
 
-      isOnline = true
+      setOnline(true)
       OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
       await OCPP20ServiceUtils.sendTransactionEvent(
@@ -166,7 +143,7 @@ await describe('E02 - OCPP 2.0.1 Offline TransactionEvent Queueing', async () =>
       expect(sentRequests.length).toBe(1)
       expect(sentRequests[0].payload.seqNo).toBe(0)
 
-      isOnline = false
+      setOnline(false)
 
       await OCPP20ServiceUtils.sendTransactionEvent(
         mockChargingStation,
@@ -194,7 +171,7 @@ await describe('E02 - OCPP 2.0.1 Offline TransactionEvent Queueing', async () =>
       const connectorId = 1
       const transactionId = generateUUID()
 
-      isOnline = false
+      setOnline(false)
       OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
       const beforeQueue = new Date()
@@ -223,7 +200,7 @@ await describe('E02 - OCPP 2.0.1 Offline TransactionEvent Queueing', async () =>
       const connectorId = 1
       const transactionId = generateUUID()
 
-      isOnline = false
+      setOnline(false)
       OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
       await OCPP20ServiceUtils.sendTransactionEvent(
@@ -244,7 +221,7 @@ await describe('E02 - OCPP 2.0.1 Offline TransactionEvent Queueing', async () =>
 
       expect(sentRequests.length).toBe(0)
 
-      isOnline = true
+      setOnline(true)
 
       await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
 
@@ -257,7 +234,7 @@ await describe('E02 - OCPP 2.0.1 Offline TransactionEvent Queueing', async () =>
       const connectorId = 1
       const transactionId = generateUUID()
 
-      isOnline = false
+      setOnline(false)
       OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
       await OCPP20ServiceUtils.sendTransactionEvent(
@@ -271,7 +248,7 @@ await describe('E02 - OCPP 2.0.1 Offline TransactionEvent Queueing', async () =>
       const connector = mockChargingStation.getConnectorStatus(connectorId)
       expect(connector?.transactionEventQueue?.length).toBe(1)
 
-      isOnline = true
+      setOnline(true)
       await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
 
       expect(connector.transactionEventQueue.length).toBe(0)
@@ -281,7 +258,7 @@ await describe('E02 - OCPP 2.0.1 Offline TransactionEvent Queueing', async () =>
       const connectorId = 1
       const transactionId = generateUUID()
 
-      isOnline = false
+      setOnline(false)
       OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
       await OCPP20ServiceUtils.sendTransactionEvent(
@@ -308,7 +285,7 @@ await describe('E02 - OCPP 2.0.1 Offline TransactionEvent Queueing', async () =>
         transactionId
       )
 
-      isOnline = true
+      setOnline(true)
       await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
 
       expect(sentRequests[0].payload.eventType).toBe(OCPP20TransactionEventEnumType.Started)
@@ -348,7 +325,7 @@ await describe('E02 - OCPP 2.0.1 Offline TransactionEvent Queueing', async () =>
       const connectorId = 1
       const transactionId = generateUUID()
 
-      isOnline = true
+      setOnline(true)
       OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
 
       await OCPP20ServiceUtils.sendTransactionEvent(
@@ -360,7 +337,7 @@ await describe('E02 - OCPP 2.0.1 Offline TransactionEvent Queueing', async () =>
       )
       expect(sentRequests[0].payload.seqNo).toBe(0)
 
-      isOnline = false
+      setOnline(false)
 
       await OCPP20ServiceUtils.sendTransactionEvent(
         mockChargingStation,
@@ -378,7 +355,7 @@ await describe('E02 - OCPP 2.0.1 Offline TransactionEvent Queueing', async () =>
         transactionId
       )
 
-      isOnline = true
+      setOnline(true)
 
       await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
 
@@ -406,7 +383,7 @@ await describe('E02 - OCPP 2.0.1 Offline TransactionEvent Queueing', async () =>
       const transactionId1 = generateUUID()
       const transactionId2 = generateUUID()
 
-      isOnline = false
+      setOnline(false)
       OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 1)
       OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 2)
 
@@ -452,7 +429,7 @@ await describe('E02 - OCPP 2.0.1 Offline TransactionEvent Queueing', async () =>
       const transactionId1 = generateUUID()
       const transactionId2 = generateUUID()
 
-      isOnline = false
+      setOnline(false)
       OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 1)
       OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 2)
 
@@ -472,7 +449,7 @@ await describe('E02 - OCPP 2.0.1 Offline TransactionEvent Queueing', async () =>
         transactionId2
       )
 
-      isOnline = true
+      setOnline(true)
 
       await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, 1)
 
index 1d2484ceea72dc847da7b61b4b0ecf431db99344..8f087569427851c0d34822289aa0c75256ace17b 100644 (file)
@@ -4,10 +4,9 @@
  */
 
 import { expect } from '@std/expect'
-import { afterEach, beforeEach, describe, it, mock } from 'node:test'
+import { afterEach, beforeEach, describe, it } from 'node:test'
 
 import type { ChargingStation } from '../../../../src/charging-station/ChargingStation.js'
-import type { EmptyObject } from '../../../../src/types/index.js'
 
 import { OCPP20ServiceUtils } from '../../../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js'
 import {
@@ -19,45 +18,21 @@ import { Constants, generateUUID } from '../../../../src/utils/index.js'
 import { standardCleanup } from '../../../../tests/helpers/TestLifecycleHelpers.js'
 import { createChargingStation } from '../../../ChargingStationFactory.js'
 import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
-import { resetLimits } from './OCPP20TestUtils.js'
+import {
+  type CapturedOCPPRequest,
+  createMockStationWithRequestTracking,
+  type MockStationWithTracking,
+} from './OCPP20TestUtils.js'
 
 await describe('E02 - OCPP 2.0.1 Periodic TransactionEvent at TxUpdatedInterval', async () => {
+  let mockTracking: MockStationWithTracking
   let mockChargingStation: ChargingStation
-  let requestHandlerMock: ReturnType<typeof mock.fn>
-  interface SentRequest {
-    command: string
-    payload: Record<string, unknown>
-  }
-  let sentRequests: SentRequest[]
+  let sentRequests: CapturedOCPPRequest[]
 
   beforeEach(() => {
-    sentRequests = []
-    requestHandlerMock = mock.fn(
-      async (_station: ChargingStation, command: string, payload: Record<string, unknown>) => {
-        sentRequests.push({ command, payload })
-        return Promise.resolve({} as EmptyObject)
-      }
-    )
-
-    mockChargingStation = createChargingStation({
-      baseName: TEST_CHARGING_STATION_BASE_NAME,
-      connectorsCount: 3,
-      evseConfiguration: { evsesCount: 3 },
-      heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
-      ocppRequestService: {
-        requestHandler: requestHandlerMock,
-      },
-      stationInfo: {
-        ocppStrictCompliance: true,
-        ocppVersion: OCPPVersion.VERSION_201,
-      },
-      websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
-    })
-
-    // Mock isWebSocketConnectionOpened to return true (online)
-    mockChargingStation.isWebSocketConnectionOpened = () => true
-
-    resetLimits(mockChargingStation)
+    mockTracking = createMockStationWithRequestTracking()
+    mockChargingStation = mockTracking.station
+    sentRequests = mockTracking.sentRequests
   })
 
   afterEach(() => {
index 3e8487be78bf4c43f6362c360f1c1644930f91c6..0451ebf4c32b56f803fc90b464322793f31955c3 100644 (file)
@@ -1,14 +1,21 @@
+import { mock } from 'node:test'
+
 import type { ChargingStation } from '../../../../src/charging-station/ChargingStation.js'
 import type { OCPP20RequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestService.js'
 import type { ConfigurationKey } from '../../../../src/types/ChargingStationOcppConfiguration.js'
 import type { EmptyObject } from '../../../../src/types/EmptyObject.js'
 import type { JsonType, OCPP20RequestCommand } from '../../../../src/types/index.js'
+import type {
+  OCPP20IdTokenType,
+  OCPP20TransactionContext,
+} from '../../../../src/types/ocpp/2.0/Transaction.js'
 
 import {
   ConnectorStatusEnum,
   OCPP20RequiredVariableName,
   OCPPVersion,
 } from '../../../../src/types/index.js'
+import { OCPP20IdTokenEnumType } from '../../../../src/types/ocpp/2.0/Transaction.js'
 import { Constants } from '../../../../src/utils/index.js'
 import { createChargingStation } from '../../../ChargingStationFactory.js'
 import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
@@ -20,6 +27,30 @@ import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConsta
 // purposes, eliminating the need for `as any` casts and eslint-disable comments.
 // ============================================================================
 
+/**
+ * Interface representing a captured OCPP request for test verification.
+ */
+export interface CapturedOCPPRequest {
+  /** The OCPP command name (e.g., 'TransactionEvent', 'Heartbeat') */
+  command: string
+  /** The request payload */
+  payload: Record<string, unknown>
+}
+
+/**
+ * Result of creating a mock station with request tracking.
+ */
+export interface MockStationWithTracking {
+  /** The mock function used as request handler */
+  requestHandlerMock: ReturnType<typeof mock.fn>
+  /** Array that captures all sent requests */
+  sentRequests: CapturedOCPPRequest[]
+  /** Function to set the station's online status */
+  setOnline: (online: boolean) => void
+  /** The mock charging station instance */
+  station: ChargingStation
+}
+
 /**
  * Interface exposing private methods of OCPP20RequestService for testing.
  * This allows type-safe testing without `as any` casts.
@@ -62,6 +93,58 @@ export function createMockOCPP20TransactionTestStation (): ChargingStation {
   })
 }
 
+/**
+ * Create a mock ChargingStation with request tracking for testing OCPP request flows.
+ * This is useful for tests that need to verify what requests were sent.
+ * @returns Object containing the station, captured requests array, and control functions
+ * @example
+ * ```typescript
+ * const { station, sentRequests, setOnline } = createMockStationWithRequestTracking()
+ * await OCPP20ServiceUtils.sendTransactionEvent(station, ...)
+ * expect(sentRequests.length).toBe(1)
+ * expect(sentRequests[0].command).toBe('TransactionEvent')
+ * ```
+ */
+export function createMockStationWithRequestTracking (): MockStationWithTracking {
+  const sentRequests: CapturedOCPPRequest[] = []
+  let isOnline = true
+
+  const requestHandlerMock = mock.fn(
+    async (_station: ChargingStation, command: string, payload: Record<string, unknown>) => {
+      sentRequests.push({ command, payload })
+      return Promise.resolve({} as EmptyObject)
+    }
+  )
+
+  const station = createChargingStation({
+    baseName: TEST_CHARGING_STATION_BASE_NAME,
+    connectorsCount: 3,
+    evseConfiguration: { evsesCount: 3 },
+    heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+    ocppRequestService: {
+      requestHandler: requestHandlerMock,
+    },
+    stationInfo: {
+      ocppStrictCompliance: true,
+      ocppVersion: OCPPVersion.VERSION_201,
+    },
+    websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+  })
+
+  station.isWebSocketConnectionOpened = () => isOnline
+
+  resetLimits(station)
+
+  return {
+    requestHandlerMock,
+    sentRequests,
+    setOnline: (online: boolean) => {
+      isOnline = online
+    },
+    station,
+  }
+}
+
 /**
  * Create a testable wrapper for OCPP20RequestService that exposes private methods.
  *
@@ -252,3 +335,237 @@ function ensureConfig (chargingStation: ChargingStation): ConfigurationKey[] {
   chargingStation.ocppConfiguration.configurationKey ??= []
   return chargingStation.ocppConfiguration.configurationKey
 }
+
+// ============================================================================
+// TransactionEvent Fixtures
+// ============================================================================
+// Pre-built fixtures for common transaction event testing patterns.
+// ============================================================================
+
+/**
+ * Pre-built IdToken fixtures for common test scenarios.
+ * Use these to avoid duplication of token creation across test files.
+ */
+export const IdTokenFixtures = {
+  /**
+   * Central (server-side) token
+   * @param idToken
+   */
+  central: (idToken = 'CENTRAL_TOKEN_001'): OCPP20IdTokenType => ({
+    idToken,
+    type: OCPP20IdTokenEnumType.Central,
+  }),
+
+  /**
+   * eMAID contract identifier token
+   * @param idToken
+   */
+  emaid: (idToken = 'DE*ABC*E123456*1'): OCPP20IdTokenType => ({
+    idToken,
+    type: OCPP20IdTokenEnumType.eMAID,
+  }),
+
+  /**
+   * ISO14443 RFID token (most common type)
+   * @param idToken
+   */
+  iso14443: (idToken = 'TEST_RFID_TOKEN_001'): OCPP20IdTokenType => ({
+    idToken,
+    type: OCPP20IdTokenEnumType.ISO14443,
+  }),
+
+  /**
+   * ISO15693 RFID token
+   * @param idToken
+   */
+  iso15693: (idToken = 'TEST_ISO15693_001'): OCPP20IdTokenType => ({
+    idToken,
+    type: OCPP20IdTokenEnumType.ISO15693,
+  }),
+
+  /** NoAuthorization token (free charging) */
+  noAuth: (): OCPP20IdTokenType => ({
+    idToken: '',
+    type: OCPP20IdTokenEnumType.NoAuthorization,
+  }),
+} as const
+
+/**
+ * Pre-built TransactionContext factories for common flow patterns.
+ * Use these to create standardized contexts for different transaction flows.
+ */
+export const TransactionContextFixtures = {
+  // ===== Local Authorization Contexts =====
+
+  /**
+   * Abnormal condition (with optional condition type)
+   * @param condition
+   */
+  abnormalCondition: (condition = 'OverCurrent'): OCPP20TransactionContext => ({
+    abnormalCondition: condition,
+    source: 'abnormal_condition',
+  }),
+
+  /** Cable plugged in (E02 cable-first start) */
+  cablePluggedIn: (): OCPP20TransactionContext => ({
+    cableState: 'plugged_in',
+    source: 'cable_action',
+  }),
+
+  /** Deauthorization (token revoked or invalid) */
+  deauthorized: (): OCPP20TransactionContext => ({
+    authorizationMethod: 'idToken',
+    isDeauthorized: true,
+    source: 'local_authorization',
+  }),
+
+  // ===== Cable Action Contexts (E02 flow) =====
+
+  /** Energy limit reached */
+  energyLimitReached: (): OCPP20TransactionContext => ({
+    source: 'energy_limit',
+  }),
+
+  /** EV communication lost */
+  evCommunicationLost: (): OCPP20TransactionContext => ({
+    source: 'system_event',
+    systemEvent: 'ev_communication_lost',
+  }),
+
+  /** EV connect timeout */
+  evConnectTimeout: (): OCPP20TransactionContext => ({
+    source: 'system_event',
+    systemEvent: 'ev_connect_timeout',
+  }),
+
+  // ===== Remote Command Contexts =====
+
+  /** Cable unplugged / EV departed */
+  evDeparted: (): OCPP20TransactionContext => ({
+    cableState: 'unplugged',
+    source: 'cable_action',
+  }),
+
+  /** EV detected after cable connection */
+  evDetected: (): OCPP20TransactionContext => ({
+    cableState: 'detected',
+    source: 'cable_action',
+  }),
+
+  /**
+   * IdToken-first authorization (E03 flow start)
+   * @param authorizationMethod
+   */
+  idTokenAuthorized: (
+    authorizationMethod: 'groupIdToken' | 'idToken' = 'idToken'
+  ): OCPP20TransactionContext => ({
+    authorizationMethod,
+    source: 'local_authorization',
+  }),
+
+  /** Clock-aligned meter value */
+  meterValueClock: (): OCPP20TransactionContext => ({
+    isPeriodicMeterValue: false,
+    source: 'meter_value',
+  }),
+
+  /** Periodic meter value (sampled interval) */
+  meterValuePeriodic: (): OCPP20TransactionContext => ({
+    isPeriodicMeterValue: true,
+    source: 'meter_value',
+  }),
+
+  // ===== Meter Value Contexts =====
+
+  /** Remote start transaction request */
+  remoteStart: (): OCPP20TransactionContext => ({
+    command: 'RequestStartTransaction',
+    source: 'remote_command',
+  }),
+
+  /** Remote stop transaction request */
+  remoteStop: (): OCPP20TransactionContext => ({
+    command: 'RequestStopTransaction',
+    source: 'remote_command',
+  }),
+
+  /** Reset command */
+  reset: (): OCPP20TransactionContext => ({
+    command: 'Reset',
+    source: 'remote_command',
+  }),
+
+  // ===== System Event Contexts =====
+
+  /** Signed data received */
+  signedData: (): OCPP20TransactionContext => ({
+    isSignedDataReceived: true,
+    source: 'meter_value',
+  }),
+
+  /** Stop authorized by local token presentation */
+  stopAuthorized: (): OCPP20TransactionContext => ({
+    authorizationMethod: 'stopAuthorized',
+    source: 'local_authorization',
+  }),
+
+  // ===== Limit Contexts =====
+
+  /** Time limit reached */
+  timeLimitReached: (): OCPP20TransactionContext => ({
+    source: 'time_limit',
+  }),
+
+  /** Trigger message command */
+  triggerMessage: (): OCPP20TransactionContext => ({
+    command: 'TriggerMessage',
+    source: 'remote_command',
+  }),
+
+  // ===== Abnormal Condition Contexts =====
+
+  /** Unlock connector command */
+  unlockConnector: (): OCPP20TransactionContext => ({
+    command: 'UnlockConnector',
+    source: 'remote_command',
+  }),
+} as const
+
+/**
+ * Type representing a transaction flow pattern for parameterized testing.
+ */
+export interface TransactionFlowPattern {
+  /** Human-readable description of the flow */
+  description: string
+  /** Expected trigger reason for Started event */
+  expectedStartTrigger: string
+  /** Whether to include idToken in Started event */
+  includeIdToken: boolean
+  /** Context to use for Started event (determines initial trigger reason) */
+  startContext: OCPP20TransactionContext
+}
+
+/**
+ * Pre-defined transaction flow patterns for parameterized testing.
+ * Covers the main OCPP 2.0.1 transaction initiation scenarios.
+ */
+export const TransactionFlowPatterns: TransactionFlowPattern[] = [
+  {
+    description: 'E02 Cable-First: CablePluggedIn → Charging → EVDeparted',
+    expectedStartTrigger: 'CablePluggedIn',
+    includeIdToken: false,
+    startContext: TransactionContextFixtures.cablePluggedIn(),
+  },
+  {
+    description: 'E03 IdToken-First: Authorized → Cable → Charging → StopAuthorized',
+    expectedStartTrigger: 'Authorized',
+    includeIdToken: true,
+    startContext: TransactionContextFixtures.idTokenAuthorized(),
+  },
+  {
+    description: 'Remote Start: RemoteStart → Charging → RemoteStop',
+    expectedStartTrigger: 'RemoteStart',
+    includeIdToken: false,
+    startContext: TransactionContextFixtures.remoteStart(),
+  },
+] as const
index bb9f7ec4131a27f1989e75bbc8d4b3ad032b2dd7..9541da7054e6ff07243a29ff4e8828dcb89e0b6d 100644 (file)
@@ -5,6 +5,7 @@ import { expect } from '@std/expect'
 import type { ChargingStation } from '../../../../../src/charging-station/ChargingStation.js'
 import type {
   AuthCache,
+  LocalAuthListManager,
   OCPPAuthAdapter,
   OCPPAuthService,
 } from '../../../../../src/charging-station/ocpp/auth/interfaces/OCPPAuthService.js'
@@ -376,3 +377,25 @@ export const createMockAuthServiceTestStation = (
       ocppVersion,
     },
   }) as unknown as ChargingStation
+
+// ============================================================================
+// LocalAuthListManager Mock
+// ============================================================================
+
+/**
+ * Create a mock LocalAuthListManager for testing.
+ * @param overrides - Partial LocalAuthListManager methods to override defaults
+ * @returns Mock LocalAuthListManager with stubbed async methods
+ */
+export const createMockLocalAuthListManager = (
+  overrides?: Partial<LocalAuthListManager>
+): LocalAuthListManager => ({
+  addEntry: async () => Promise.resolve(),
+  clearAll: async () => Promise.resolve(),
+  getAllEntries: async () => Promise.resolve([]),
+  getEntry: async () => Promise.resolve(undefined),
+  getVersion: async () => Promise.resolve(1),
+  removeEntry: async () => Promise.resolve(),
+  updateVersion: async () => Promise.resolve(),
+  ...overrides,
+})
index 50ca4ddc3ab8ef0472db9fedd1ddb94a74044668..da17e16ebdb2f0bd3ebb648967342b77fe71e156 100644 (file)
@@ -10,13 +10,17 @@ import type { OCPPAuthAdapter } from '../../../../../src/charging-station/ocpp/a
 
 import { CertificateAuthStrategy } from '../../../../../src/charging-station/ocpp/auth/strategies/CertificateAuthStrategy.js'
 import {
-  type AuthConfiguration,
   AuthenticationMethod,
   AuthorizationStatus,
   IdentifierType,
 } from '../../../../../src/charging-station/ocpp/auth/types/AuthTypes.js'
 import { OCPPVersion } from '../../../../../src/types/ocpp/OCPPVersion.js'
-import { createMockAuthorizationResult, createMockAuthRequest } from '../helpers/MockFactories.js'
+import {
+  createMockAuthorizationResult,
+  createMockAuthRequest,
+  createMockOCPPAdapter,
+  createTestAuthConfig,
+} from '../helpers/MockFactories.js'
 
 await describe('CertificateAuthStrategy', async () => {
   let strategy: CertificateAuthStrategy
@@ -24,7 +28,6 @@ await describe('CertificateAuthStrategy', async () => {
   let mockOCPP20Adapter: OCPPAuthAdapter
 
   beforeEach(() => {
-    // Create mock charging station
     mockChargingStation = {
       logPrefix: () => '[TEST-CS-001]',
       stationInfo: {
@@ -33,25 +36,19 @@ await describe('CertificateAuthStrategy', async () => {
       },
     } as unknown as ChargingStation
 
-    // Create mock OCPP 2.0 adapter (certificate auth only in 2.0+)
-    mockOCPP20Adapter = {
+    mockOCPP20Adapter = createMockOCPPAdapter(OCPPVersion.VERSION_20, {
       authorizeRemote: async () =>
         Promise.resolve(
           createMockAuthorizationResult({
             method: AuthenticationMethod.CERTIFICATE_BASED,
           })
         ),
-      convertFromUnifiedIdentifier: identifier => identifier,
       convertToUnifiedIdentifier: identifier => ({
         ocppVersion: OCPPVersion.VERSION_20,
         type: IdentifierType.CERTIFICATE,
         value: typeof identifier === 'string' ? identifier : JSON.stringify(identifier),
       }),
-      getConfigurationSchema: () => ({}),
-      isRemoteAvailable: async () => Promise.resolve(true),
-      ocppVersion: OCPPVersion.VERSION_20,
-      validateConfiguration: async () => Promise.resolve(true),
-    }
+    })
 
     const adapters = new Map<OCPPVersion, OCPPAuthAdapter>()
     adapters.set(OCPPVersion.VERSION_20, mockOCPP20Adapter)
@@ -73,59 +70,24 @@ await describe('CertificateAuthStrategy', async () => {
 
   await describe('initialize', async () => {
     await it('should initialize successfully when certificate auth is enabled', async () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: true,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig({ certificateAuthEnabled: true })
       await expect(strategy.initialize(config)).resolves.toBeUndefined()
     })
 
     await it('should handle disabled certificate auth gracefully', async () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig({ certificateAuthEnabled: false })
       await expect(strategy.initialize(config)).resolves.toBeUndefined()
     })
   })
 
   await describe('canHandle', async () => {
     beforeEach(async () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: true,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
+      const config = createTestAuthConfig({ certificateAuthEnabled: true })
       await strategy.initialize(config)
     })
 
     await it('should return true for certificate identifiers with OCPP 2.0', () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: true,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig({ certificateAuthEnabled: true })
       const request = createMockAuthRequest({
         identifier: {
           certificateHashData: {
@@ -144,16 +106,7 @@ await describe('CertificateAuthStrategy', async () => {
     })
 
     await it('should return false for non-certificate identifiers', () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: true,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig({ certificateAuthEnabled: true })
       const request = createMockAuthRequest({
         identifier: {
           ocppVersion: OCPPVersion.VERSION_20,
@@ -166,16 +119,7 @@ await describe('CertificateAuthStrategy', async () => {
     })
 
     await it('should return false for OCPP 1.6', () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: true,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig({ certificateAuthEnabled: true })
       const request = createMockAuthRequest({
         identifier: {
           ocppVersion: OCPPVersion.VERSION_16,
@@ -188,16 +132,7 @@ await describe('CertificateAuthStrategy', async () => {
     })
 
     await it('should return false when certificate auth is disabled', () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig({ certificateAuthEnabled: false })
       const request = createMockAuthRequest({
         identifier: {
           certificateHashData: {
@@ -216,16 +151,7 @@ await describe('CertificateAuthStrategy', async () => {
     })
 
     await it('should return false when missing certificate data', () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: true,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig({ certificateAuthEnabled: true })
       const request = createMockAuthRequest({
         identifier: {
           ocppVersion: OCPPVersion.VERSION_20,
@@ -240,29 +166,12 @@ await describe('CertificateAuthStrategy', async () => {
 
   await describe('authenticate', async () => {
     beforeEach(async () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: true,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
+      const config = createTestAuthConfig({ certificateAuthEnabled: true })
       await strategy.initialize(config)
     })
 
     await it('should authenticate valid test certificate', async () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: true,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig({ certificateAuthEnabled: true })
       const request = createMockAuthRequest({
         identifier: {
           certificateHashData: {
@@ -285,16 +194,7 @@ await describe('CertificateAuthStrategy', async () => {
     })
 
     await it('should reject invalid certificate serial numbers', async () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: true,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig({ certificateAuthEnabled: true })
       const request = createMockAuthRequest({
         identifier: {
           certificateHashData: {
@@ -316,16 +216,7 @@ await describe('CertificateAuthStrategy', async () => {
     })
 
     await it('should reject revoked certificates', async () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: true,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig({ certificateAuthEnabled: true })
       const request = createMockAuthRequest({
         identifier: {
           certificateHashData: {
@@ -347,16 +238,7 @@ await describe('CertificateAuthStrategy', async () => {
     })
 
     await it('should handle missing certificate data', async () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: true,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig({ certificateAuthEnabled: true })
       const request = createMockAuthRequest({
         identifier: {
           ocppVersion: OCPPVersion.VERSION_20,
@@ -372,16 +254,7 @@ await describe('CertificateAuthStrategy', async () => {
     })
 
     await it('should handle invalid hash algorithm', async () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: true,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig({ certificateAuthEnabled: true })
       const request = createMockAuthRequest({
         identifier: {
           certificateHashData: {
@@ -403,16 +276,7 @@ await describe('CertificateAuthStrategy', async () => {
     })
 
     await it('should handle invalid hash format', async () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: true,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig({ certificateAuthEnabled: true })
       const request = createMockAuthRequest({
         identifier: {
           certificateHashData: {
@@ -445,26 +309,9 @@ await describe('CertificateAuthStrategy', async () => {
     })
 
     await it('should update stats after authentication', async () => {
-      await strategy.initialize({
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: true,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      })
-
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: true,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
+      await strategy.initialize(createTestAuthConfig({ certificateAuthEnabled: true }))
 
+      const config = createTestAuthConfig({ certificateAuthEnabled: true })
       const request = createMockAuthRequest({
         identifier: {
           certificateHashData: {
@@ -489,15 +336,7 @@ await describe('CertificateAuthStrategy', async () => {
 
   await describe('cleanup', async () => {
     await it('should reset strategy state', async () => {
-      await strategy.initialize({
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: true,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      })
+      await strategy.initialize(createTestAuthConfig({ certificateAuthEnabled: true }))
 
       await strategy.cleanup()
       const stats = await strategy.getStats()
index 04f8c0d207076b026606997068485d88b592d3cf..3701029ff049aafa6ad96c7cc8e9f1f843f8815a 100644 (file)
@@ -12,16 +12,18 @@ import type {
 
 import { LocalAuthStrategy } from '../../../../../src/charging-station/ocpp/auth/strategies/LocalAuthStrategy.js'
 import {
-  type AuthConfiguration,
   AuthContext,
   AuthenticationMethod,
   AuthorizationStatus,
   IdentifierType,
 } from '../../../../../src/charging-station/ocpp/auth/types/AuthTypes.js'
 import {
+  createMockAuthCache,
   createMockAuthorizationResult,
   createMockAuthRequest,
+  createMockLocalAuthListManager,
   createMockOCPP16Identifier,
+  createTestAuthConfig,
 } from '../helpers/MockFactories.js'
 
 await describe('LocalAuthStrategy', async () => {
@@ -30,50 +32,8 @@ await describe('LocalAuthStrategy', async () => {
   let mockLocalAuthListManager: LocalAuthListManager
 
   beforeEach(() => {
-    // Create mock auth cache
-    mockAuthCache = {
-      clear: async () => {
-        // Mock implementation
-      },
-      // eslint-disable-next-line @typescript-eslint/require-await
-      get: async (_key: string) => undefined,
-      getStats: async () =>
-        await Promise.resolve({
-          expiredEntries: 0,
-          hitRate: 0,
-          hits: 0,
-          memoryUsage: 0,
-          misses: 0,
-          totalEntries: 0,
-        }),
-      remove: async (_key: string) => {
-        // Mock implementation
-      },
-      set: async (_key: string, _value, _ttl?: number) => {
-        // Mock implementation
-      },
-    }
-
-    // Create mock local auth list manager
-    mockLocalAuthListManager = {
-      addEntry: async _entry => {
-        // Mock implementation
-      },
-      clearAll: async () => {
-        // Mock implementation
-      },
-      getAllEntries: async () => await Promise.resolve([]),
-      // eslint-disable-next-line @typescript-eslint/require-await
-      getEntry: async (_identifier: string) => undefined,
-      getVersion: async () => await Promise.resolve(1),
-      removeEntry: async (_identifier: string) => {
-        // Mock implementation
-      },
-      updateVersion: async (_version: number) => {
-        // Mock implementation
-      },
-    }
-
+    mockAuthCache = createMockAuthCache()
+    mockLocalAuthListManager = createMockLocalAuthListManager()
     strategy = new LocalAuthStrategy(mockLocalAuthListManager, mockAuthCache)
   })
 
@@ -96,92 +56,51 @@ await describe('LocalAuthStrategy', async () => {
 
   await describe('initialize', async () => {
     await it('should initialize successfully with valid config', async () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
+      const config = createTestAuthConfig({
         authorizationCacheEnabled: true,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
         localAuthListEnabled: true,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      })
       await expect(strategy.initialize(config)).resolves.toBeUndefined()
     })
   })
 
   await describe('canHandle', async () => {
     await it('should return true when local auth list is enabled', () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: true,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig({ localAuthListEnabled: true })
       const request = createMockAuthRequest({
         identifier: createMockOCPP16Identifier('TEST_TAG', IdentifierType.ID_TAG),
       })
-
       expect(strategy.canHandle(request, config)).toBe(true)
     })
 
     await it('should return true when cache is enabled', () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: true,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig({ authorizationCacheEnabled: true })
       const request = createMockAuthRequest({
         identifier: createMockOCPP16Identifier('TEST_TAG', IdentifierType.ID_TAG),
       })
-
       expect(strategy.canHandle(request, config)).toBe(true)
     })
 
     await it('should return false when nothing is enabled', () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig()
       const request = createMockAuthRequest({
         identifier: createMockOCPP16Identifier('TEST_TAG', IdentifierType.ID_TAG),
       })
-
       expect(strategy.canHandle(request, config)).toBe(false)
     })
   })
 
   await describe('authenticate', async () => {
     beforeEach(async () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
+      const config = createTestAuthConfig({
         authorizationCacheEnabled: true,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
         localAuthListEnabled: true,
-        localPreAuthorize: false,
         offlineAuthorizationEnabled: true,
-      }
+      })
       await strategy.initialize(config)
     })
 
     await it('should authenticate using local auth list', async () => {
-      // Mock local auth list entry
       mockLocalAuthListManager.getEntry = async () =>
         await Promise.resolve({
           expiryDate: new Date(Date.now() + 86400000),
@@ -190,16 +109,10 @@ await describe('LocalAuthStrategy', async () => {
           status: 'accepted',
         })
 
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
+      const config = createTestAuthConfig({
         authorizationCacheEnabled: true,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
         localAuthListEnabled: true,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      })
       const request = createMockAuthRequest({
         identifier: createMockOCPP16Identifier('LOCAL_TAG', IdentifierType.ID_TAG),
       })
@@ -212,26 +125,16 @@ await describe('LocalAuthStrategy', async () => {
     })
 
     await it('should authenticate using cache', async () => {
-      // Mock cache hit
       mockAuthCache.get = async () =>
         await Promise.resolve(
           createMockAuthorizationResult({
             cacheTtl: 300,
             method: AuthenticationMethod.CACHE,
-            timestamp: new Date(Date.now() - 60000), // 1 minute ago
+            timestamp: new Date(Date.now() - 60000),
           })
         )
 
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: true,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig({ authorizationCacheEnabled: true })
       const request = createMockAuthRequest({
         identifier: createMockOCPP16Identifier('CACHED_TAG', IdentifierType.ID_TAG),
       })
@@ -244,16 +147,7 @@ await describe('LocalAuthStrategy', async () => {
     })
 
     await it('should use offline fallback for transaction stop', async () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: true,
-      }
-
+      const config = createTestAuthConfig({ offlineAuthorizationEnabled: true })
       const request = createMockAuthRequest({
         allowOffline: true,
         context: AuthContext.TRANSACTION_STOP,
@@ -269,16 +163,7 @@ await describe('LocalAuthStrategy', async () => {
     })
 
     await it('should return undefined when no local auth available', async () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig()
       const request = createMockAuthRequest({
         identifier: createMockOCPP16Identifier('UNKNOWN_TAG', IdentifierType.ID_TAG),
       })
@@ -290,7 +175,7 @@ await describe('LocalAuthStrategy', async () => {
 
   await describe('cacheResult', async () => {
     await it('should cache authorization result', async () => {
-      let cachedValue
+      let cachedValue: undefined | { key: string; ttl?: number; value: unknown }
       // eslint-disable-next-line @typescript-eslint/require-await
       mockAuthCache.set = async (key: string, value, ttl?: number) => {
         cachedValue = { key, ttl, value }
index b1d6002c861eec449bb8c3b690924d4650777ea3..4a35c40323f4183c0d8b04896d500405f722aaa3 100644 (file)
@@ -12,17 +12,17 @@ import type {
 
 import { RemoteAuthStrategy } from '../../../../../src/charging-station/ocpp/auth/strategies/RemoteAuthStrategy.js'
 import {
-  type AuthConfiguration,
   AuthenticationMethod,
   AuthorizationStatus,
   IdentifierType,
-  type UnifiedIdentifier,
 } from '../../../../../src/charging-station/ocpp/auth/types/AuthTypes.js'
 import { OCPPVersion } from '../../../../../src/types/ocpp/OCPPVersion.js'
 import {
-  createMockAuthorizationResult,
+  createMockAuthCache,
   createMockAuthRequest,
   createMockOCPP16Identifier,
+  createMockOCPPAdapter,
+  createTestAuthConfig,
 } from '../helpers/MockFactories.js'
 
 await describe('RemoteAuthStrategy', async () => {
@@ -32,75 +32,9 @@ await describe('RemoteAuthStrategy', async () => {
   let mockOCPP20Adapter: OCPPAuthAdapter
 
   beforeEach(() => {
-    // Create mock auth cache
-    mockAuthCache = {
-      clear: async () => Promise.resolve(),
-      get: async (key: string) => Promise.resolve(undefined),
-      getStats: async () =>
-        Promise.resolve({
-          expiredEntries: 0,
-          hitRate: 0,
-          hits: 0,
-          memoryUsage: 0,
-          misses: 0,
-          totalEntries: 0,
-        }),
-      remove: async (key: string) => Promise.resolve(),
-      set: async (key: string, value, ttl?: number) => Promise.resolve(),
-    }
-
-    // Create mock OCPP 1.6 adapter
-    mockOCPP16Adapter = {
-      authorizeRemote: async (identifier: UnifiedIdentifier) =>
-        Promise.resolve(
-          createMockAuthorizationResult({
-            method: AuthenticationMethod.REMOTE_AUTHORIZATION,
-          })
-        ),
-      convertFromUnifiedIdentifier: (identifier: UnifiedIdentifier) => identifier.value,
-      convertToUnifiedIdentifier: (identifier: object | string) => ({
-        ocppVersion: OCPPVersion.VERSION_16,
-        type: IdentifierType.ID_TAG,
-        value: typeof identifier === 'string' ? identifier : JSON.stringify(identifier),
-      }),
-      getConfigurationSchema: () => ({}),
-      isRemoteAvailable: async () => Promise.resolve(true),
-      ocppVersion: OCPPVersion.VERSION_16,
-      validateConfiguration: async (config: AuthConfiguration) => Promise.resolve(true),
-    }
-
-    // Create mock OCPP 2.0 adapter
-    mockOCPP20Adapter = {
-      authorizeRemote: async (identifier: UnifiedIdentifier) =>
-        Promise.resolve(
-          createMockAuthorizationResult({
-            method: AuthenticationMethod.REMOTE_AUTHORIZATION,
-          })
-        ),
-      convertFromUnifiedIdentifier: (identifier: UnifiedIdentifier) => ({
-        idToken: identifier.value,
-        type: identifier.type,
-      }),
-      convertToUnifiedIdentifier: (identifier: object | string) => {
-        if (typeof identifier === 'string') {
-          return {
-            ocppVersion: OCPPVersion.VERSION_20,
-            type: IdentifierType.ID_TAG,
-            value: identifier,
-          }
-        }
-        const idTokenObj = identifier as { idToken?: string }
-        return {
-          ocppVersion: OCPPVersion.VERSION_20,
-          type: IdentifierType.ID_TAG,
-          value: idTokenObj.idToken ?? 'unknown',
-        }
-      },
-      getConfigurationSchema: () => ({}),
-      isRemoteAvailable: async () => Promise.resolve(true),
-      ocppVersion: OCPPVersion.VERSION_20,
-      validateConfiguration: async (config: AuthConfiguration) => Promise.resolve(true),
-    }
+    mockAuthCache = createMockAuthCache()
+    mockOCPP16Adapter = createMockOCPPAdapter(OCPPVersion.VERSION_16)
+    mockOCPP20Adapter = createMockOCPPAdapter(OCPPVersion.VERSION_20)
 
     const adapters = new Map<OCPPVersion, OCPPAuthAdapter>()
     adapters.set(OCPPVersion.VERSION_16, mockOCPP16Adapter)
@@ -130,119 +64,56 @@ await describe('RemoteAuthStrategy', async () => {
 
   await describe('initialize', async () => {
     await it('should initialize successfully with adapters', async () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: true,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig({ authorizationCacheEnabled: true })
       await expect(strategy.initialize(config)).resolves.toBeUndefined()
     })
 
     await it('should validate adapter configurations', async () => {
       mockOCPP16Adapter.validateConfiguration = async () => Promise.resolve(true)
       mockOCPP20Adapter.validateConfiguration = async () => Promise.resolve(true)
-
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig()
       await expect(strategy.initialize(config)).resolves.toBeUndefined()
     })
   })
 
   await describe('canHandle', async () => {
     await it('should return true when remote auth is enabled', () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig()
       const request = createMockAuthRequest({
         identifier: createMockOCPP16Identifier('REMOTE_TAG', IdentifierType.ID_TAG),
       })
-
       expect(strategy.canHandle(request, config)).toBe(true)
     })
 
     await it('should return false when localPreAuthorize is enabled', () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
+      const config = createTestAuthConfig({
         localAuthListEnabled: true,
         localPreAuthorize: true,
-        offlineAuthorizationEnabled: false,
-      }
-
+      })
       const request = createMockAuthRequest({
         identifier: createMockOCPP16Identifier('REMOTE_TAG', IdentifierType.ID_TAG),
       })
-
       expect(strategy.canHandle(request, config)).toBe(false)
     })
 
     await it('should return false when no adapter available', () => {
       const strategyNoAdapters = new RemoteAuthStrategy()
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig()
       const request = createMockAuthRequest({
         identifier: createMockOCPP16Identifier('REMOTE_TAG', IdentifierType.ID_TAG),
       })
-
       expect(strategyNoAdapters.canHandle(request, config)).toBe(false)
     })
   })
 
   await describe('authenticate', async () => {
     beforeEach(async () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: true,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
+      const config = createTestAuthConfig({ authorizationCacheEnabled: true })
       await strategy.initialize(config)
     })
 
     await it('should authenticate using OCPP 1.6 adapter', async () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: true,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig({ authorizationCacheEnabled: true })
       const request = createMockAuthRequest({
         identifier: createMockOCPP16Identifier('REMOTE_TAG', IdentifierType.ID_TAG),
       })
@@ -255,16 +126,7 @@ await describe('RemoteAuthStrategy', async () => {
     })
 
     await it('should authenticate using OCPP 2.0 adapter', async () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: true,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig({ authorizationCacheEnabled: true })
       const request = createMockAuthRequest({
         identifier: {
           ocppVersion: OCPPVersion.VERSION_20,
@@ -282,22 +144,15 @@ await describe('RemoteAuthStrategy', async () => {
 
     await it('should cache successful authorization results', async () => {
       let cachedKey: string | undefined
-      mockAuthCache.set = async (key: string, value, ttl?: number) => {
+      mockAuthCache.set = async (key: string) => {
         cachedKey = key
         return Promise.resolve()
       }
 
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
+      const config = createTestAuthConfig({
         authorizationCacheEnabled: true,
         authorizationCacheLifetime: 300,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      })
       const request = createMockAuthRequest({
         identifier: createMockOCPP16Identifier('CACHE_TAG', IdentifierType.ID_TAG),
       })
@@ -309,16 +164,7 @@ await describe('RemoteAuthStrategy', async () => {
     await it('should return undefined when remote is unavailable', async () => {
       mockOCPP16Adapter.isRemoteAvailable = async () => Promise.resolve(false)
 
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig()
       const request = createMockAuthRequest({
         identifier: createMockOCPP16Identifier('UNAVAILABLE_TAG', IdentifierType.ID_TAG),
       })
@@ -328,16 +174,7 @@ await describe('RemoteAuthStrategy', async () => {
     })
 
     await it('should return undefined when no adapter available', async () => {
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig()
       const request = createMockAuthRequest({
         identifier: {
           ocppVersion: OCPPVersion.VERSION_201,
@@ -355,16 +192,7 @@ await describe('RemoteAuthStrategy', async () => {
         throw new Error('Network error')
       }
 
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig()
       const request = createMockAuthRequest({
         identifier: createMockOCPP16Identifier('ERROR_TAG', IdentifierType.ID_TAG),
       })
@@ -379,16 +207,7 @@ await describe('RemoteAuthStrategy', async () => {
       const newStrategy = new RemoteAuthStrategy()
       newStrategy.addAdapter(OCPPVersion.VERSION_16, mockOCPP16Adapter)
 
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig()
       const request = createMockAuthRequest({
         identifier: createMockOCPP16Identifier('TEST', IdentifierType.ID_TAG),
       })
@@ -399,16 +218,7 @@ await describe('RemoteAuthStrategy', async () => {
     await it('should remove adapter', () => {
       void strategy.removeAdapter(OCPPVersion.VERSION_16)
 
-      const config: AuthConfiguration = {
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      }
-
+      const config = createTestAuthConfig()
       const request = createMockAuthRequest({
         identifier: createMockOCPP16Identifier('TEST', IdentifierType.ID_TAG),
       })
@@ -419,16 +229,7 @@ await describe('RemoteAuthStrategy', async () => {
 
   await describe('testConnectivity', async () => {
     await it('should test connectivity successfully', async () => {
-      await strategy.initialize({
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      })
-
+      await strategy.initialize(createTestAuthConfig())
       const result = await strategy.testConnectivity()
       expect(result).toBe(true)
     })
@@ -443,16 +244,7 @@ await describe('RemoteAuthStrategy', async () => {
       mockOCPP16Adapter.isRemoteAvailable = async () => Promise.resolve(false)
       mockOCPP20Adapter.isRemoteAvailable = async () => Promise.resolve(false)
 
-      await strategy.initialize({
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      })
-
+      await strategy.initialize(createTestAuthConfig())
       const result = await strategy.testConnectivity()
       expect(result).toBe(false)
     })
@@ -471,16 +263,7 @@ await describe('RemoteAuthStrategy', async () => {
     })
 
     await it('should include adapter statistics', async () => {
-      await strategy.initialize({
-        allowOfflineTxForUnknownId: false,
-        authorizationCacheEnabled: false,
-        authorizationTimeout: 30,
-        certificateAuthEnabled: false,
-        localAuthListEnabled: false,
-        localPreAuthorize: false,
-        offlineAuthorizationEnabled: false,
-      })
-
+      await strategy.initialize(createTestAuthConfig())
       const stats = await strategy.getStats()
       expect(stats.adapterStats).toBeDefined()
     })
index da06d23c059a7a3952ff611bb627f3270883c1a3..9c1c2e3d6ac9deada9ca2d39d72cc76b61ea5b7c 100644 (file)
@@ -72,6 +72,19 @@ interface MockContext {
   }
 }
 
+/**
+ * Test context type for timer operations
+ */
+interface TimerTestContext {
+  mock: {
+    timers: {
+      enable: (options: { apis: MockableTimerAPI[] }) => void
+      reset: () => void
+      tick: (ms: number) => void
+    }
+  }
+}
+
 /**
  * Clear transaction state from a connector
  * @param station - ChargingStation instance
@@ -174,6 +187,49 @@ export function createLoggerMocks (
   }
 }
 
+/**
+ * Create a timer scope for manual control over timer mocking.
+ *
+ * Use this when you need more control than withMockTimers provides,
+ * such as calling tick() multiple times or conditional cleanup.
+ * @param t - Test context from node:test
+ * @param apis - Timer APIs to mock (default: setTimeout, setInterval)
+ * @returns Timer scope with tick and cleanup methods
+ * @example
+ * ```typescript
+ * await it('should test intervals', t => {
+ *   const timers = createTimerScope(t, ['setInterval'])
+ *   try {
+ *     startHeartbeat()
+ *     timers.tick(5000)
+ *     expect(heartbeatCount).toBe(5)
+ *     timers.tick(5000)
+ *     expect(heartbeatCount).toBe(10)
+ *   } finally {
+ *     timers.cleanup()
+ *   }
+ * })
+ * ```
+ */
+export function createTimerScope (
+  t: TimerTestContext,
+  apis: MockableTimerAPI[] = ['setTimeout', 'setInterval']
+): { cleanup: () => void; tick: (ms: number) => void } {
+  t.mock.timers.enable({ apis })
+  return {
+    cleanup: () => {
+      try {
+        t.mock.timers.reset()
+      } catch {
+        // Timers may already be reset, ignore
+      }
+    },
+    tick: (ms: number) => {
+      t.mock.timers.tick(ms)
+    },
+  }
+}
+
 /**
  * Setup a connector with an active transaction
  *
@@ -243,3 +299,36 @@ export function standardCleanup (): void {
   MockSharedLRUCache.resetInstance()
   MockIdTagsCache.resetInstance()
 }
+
+/**
+ * Execute a test function with mocked timers, ensuring cleanup on success or failure.
+ *
+ * This is the recommended pattern for tests that need timer mocking.
+ * It handles setup and teardown automatically with try/finally.
+ * @param t - Test context from node:test
+ * @param apis - Timer APIs to mock (default: setTimeout, setInterval)
+ * @param fn - Test function to execute
+ * @returns Result of the test function
+ * @example
+ * ```typescript
+ * await it('should handle timeout', async t => {
+ *   await withMockTimers(t, ['setTimeout'], async () => {
+ *     const promise = sleep(1000)
+ *     t.mock.timers.tick(1000)
+ *     await promise
+ *   })
+ * })
+ * ```
+ */
+export async function withMockTimers<T> (
+  t: TimerTestContext,
+  apis: MockableTimerAPI[],
+  fn: () => Promise<T> | T
+): Promise<T> {
+  t.mock.timers.enable({ apis })
+  try {
+    return await fn()
+  } finally {
+    t.mock.timers.reset()
+  }
+}
index 69da07dddc745c5ff500b739d2b7b28a54e01936..2d9c31faec4a33c6d5d1891d28e8049d9fb3b23e 100644 (file)
@@ -43,7 +43,7 @@ import {
   validateIdentifierString,
   validateUUID,
 } from '../../src/utils/Utils.js'
-import { standardCleanup } from '../helpers/TestLifecycleHelpers.js'
+import { standardCleanup, withMockTimers } from '../helpers/TestLifecycleHelpers.js'
 
 await describe('Utils test suite', async () => {
   afterEach(() => {
@@ -115,8 +115,7 @@ await describe('Utils test suite', async () => {
      * - Async functions that depend on timing
      * - Avoiding slow tests caused by actual delays
      */
-    t.mock.timers.enable({ apis: ['setTimeout'] })
-    try {
+    await withMockTimers(t, ['setTimeout'], async () => {
       const delay = 10
       const sleepPromise = sleep(delay)
       t.mock.timers.tick(delay)
@@ -124,9 +123,7 @@ await describe('Utils test suite', async () => {
       expect(timeout).toBeDefined()
       expect(typeof timeout).toBe('object')
       clearTimeout(timeout)
-    } finally {
-      t.mock.timers.reset()
-    }
+    })
   })
 
   await it('should format milliseconds duration into human readable string', () => {
index 0730797f90c7613267d3c34c3e76e597e6a335ca..a80f256f7525647cead7fc62f6be7c8af9c7233b 100644 (file)
@@ -13,7 +13,7 @@ import {
   randomizeDelay,
   sleep,
 } from '../../src/worker/WorkerUtils.js'
-import { standardCleanup } from '../helpers/TestLifecycleHelpers.js'
+import { standardCleanup, withMockTimers } from '../helpers/TestLifecycleHelpers.js'
 
 await describe('WorkerUtils test suite', async () => {
   afterEach(() => {
@@ -39,8 +39,7 @@ await describe('WorkerUtils test suite', async () => {
   })
 
   await it('should return timeout object after specified delay', async t => {
-    t.mock.timers.enable({ apis: ['setTimeout'] })
-    try {
+    await withMockTimers(t, ['setTimeout'], async () => {
       const delay = 10 // 10ms for fast test execution
       const sleepPromise = sleep(delay)
       t.mock.timers.tick(delay)
@@ -52,9 +51,7 @@ await describe('WorkerUtils test suite', async () => {
 
       // Clean up timeout
       clearTimeout(timeout)
-    } finally {
-      t.mock.timers.reset()
-    }
+    })
   })
 
   await it('should log info for success/termination codes, error for other codes', t => {