From: Jérôme Benoit Date: Sat, 28 Feb 2026 14:31:06 +0000 (+0100) Subject: refactor(tests): add shared test helpers and fixtures for DRY compliance X-Git-Tag: ocpp-server@v3.0.0~88 X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=950460d07dca174b4d474b0c926d0157c3664ee4;p=e-mobility-charging-stations-simulator.git refactor(tests): add shared test helpers and fixtures for DRY compliance - 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 --- diff --git a/tests/charging-station/ChargingStation-Configuration.test.ts b/tests/charging-station/ChargingStation-Configuration.test.ts index 66d2ba79..09ff83ca 100644 --- a/tests/charging-station/ChargingStation-Configuration.test.ts +++ b/tests/charging-station/ChargingStation-Configuration.test.ts @@ -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() - } + }) }) }) }) diff --git a/tests/charging-station/ChargingStation-Transactions.test.ts b/tests/charging-station/ChargingStation-Transactions.test.ts index 2035c3ac..b125c8a8 100644 --- a/tests/charging-station/ChargingStation-Transactions.test.ts +++ b/tests/charging-station/ChargingStation-Transactions.test.ts @@ -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() - } + }) }) }) }) diff --git a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-CableFirst.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-CableFirst.test.ts index 17c6048e..7c8300f5 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-CableFirst.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-CableFirst.test.ts @@ -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) diff --git a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-IdTokenFirst.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-IdTokenFirst.test.ts index 3212b638..9830cc02 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-IdTokenFirst.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-IdTokenFirst.test.ts @@ -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) diff --git a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-Offline.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-Offline.test.ts index 653f7d6b..250815d3 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-Offline.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-Offline.test.ts @@ -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 - interface SentRequest { - command: string - payload: Record - } - 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) => { - 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) diff --git a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-Periodic.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-Periodic.test.ts index 1d2484ce..8f087569 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-Periodic.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-Periodic.test.ts @@ -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 - interface SentRequest { - command: string - payload: Record - } - let sentRequests: SentRequest[] + let sentRequests: CapturedOCPPRequest[] beforeEach(() => { - sentRequests = [] - requestHandlerMock = mock.fn( - async (_station: ChargingStation, command: string, payload: Record) => { - 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(() => { diff --git a/tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts b/tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts index 3e8487be..0451ebf4 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts @@ -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 +} + +/** + * Result of creating a mock station with request tracking. + */ +export interface MockStationWithTracking { + /** The mock function used as request handler */ + requestHandlerMock: ReturnType + /** 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) => { + 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 diff --git a/tests/charging-station/ocpp/auth/helpers/MockFactories.ts b/tests/charging-station/ocpp/auth/helpers/MockFactories.ts index bb9f7ec4..9541da70 100644 --- a/tests/charging-station/ocpp/auth/helpers/MockFactories.ts +++ b/tests/charging-station/ocpp/auth/helpers/MockFactories.ts @@ -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 => ({ + 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, +}) diff --git a/tests/charging-station/ocpp/auth/strategies/CertificateAuthStrategy.test.ts b/tests/charging-station/ocpp/auth/strategies/CertificateAuthStrategy.test.ts index 50ca4ddc..da17e16e 100644 --- a/tests/charging-station/ocpp/auth/strategies/CertificateAuthStrategy.test.ts +++ b/tests/charging-station/ocpp/auth/strategies/CertificateAuthStrategy.test.ts @@ -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() 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() diff --git a/tests/charging-station/ocpp/auth/strategies/LocalAuthStrategy.test.ts b/tests/charging-station/ocpp/auth/strategies/LocalAuthStrategy.test.ts index 04f8c0d2..3701029f 100644 --- a/tests/charging-station/ocpp/auth/strategies/LocalAuthStrategy.test.ts +++ b/tests/charging-station/ocpp/auth/strategies/LocalAuthStrategy.test.ts @@ -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 } diff --git a/tests/charging-station/ocpp/auth/strategies/RemoteAuthStrategy.test.ts b/tests/charging-station/ocpp/auth/strategies/RemoteAuthStrategy.test.ts index b1d6002c..4a35c403 100644 --- a/tests/charging-station/ocpp/auth/strategies/RemoteAuthStrategy.test.ts +++ b/tests/charging-station/ocpp/auth/strategies/RemoteAuthStrategy.test.ts @@ -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() 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() }) diff --git a/tests/helpers/TestLifecycleHelpers.ts b/tests/helpers/TestLifecycleHelpers.ts index da06d23c..9c1c2e3d 100644 --- a/tests/helpers/TestLifecycleHelpers.ts +++ b/tests/helpers/TestLifecycleHelpers.ts @@ -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: TimerTestContext, + apis: MockableTimerAPI[], + fn: () => Promise | T +): Promise { + t.mock.timers.enable({ apis }) + try { + return await fn() + } finally { + t.mock.timers.reset() + } +} diff --git a/tests/utils/Utils.test.ts b/tests/utils/Utils.test.ts index 69da07dd..2d9c31fa 100644 --- a/tests/utils/Utils.test.ts +++ b/tests/utils/Utils.test.ts @@ -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', () => { diff --git a/tests/worker/WorkerUtils.test.ts b/tests/worker/WorkerUtils.test.ts index 0730797f..a80f256f 100644 --- a/tests/worker/WorkerUtils.test.ts +++ b/tests/worker/WorkerUtils.test.ts @@ -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 => {