- 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
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
}
})
- 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
// 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
// Assert - should complete without error
expect(station).toBeDefined()
- } finally {
- t.mock.timers.reset()
- }
+ })
})
})
})
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 () => {
}
})
- 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
// 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
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
// 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
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
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
// 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
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
// Assert - interval should be cleared
expect(connector1?.transactionTxUpdatedSetInterval).toBeUndefined()
- } finally {
- t.mock.timers.reset()
- }
+ })
})
})
})
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'
/**
// =========================================================================
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)
createMockOCPP20TransactionTestStation,
resetConnectorTransactionState,
resetLimits,
+ TransactionContextFixtures,
} from './OCPP20TestUtils.js'
/**
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)
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(() => {
const connectorId = 1
const transactionId = generateUUID()
- isOnline = false
+ setOnline(false)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
const connectorId = 1
const transactionId = generateUUID()
- isOnline = false
+ setOnline(false)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
const connectorId = 1
const transactionId = generateUUID()
- isOnline = true
+ setOnline(true)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
await OCPP20ServiceUtils.sendTransactionEvent(
expect(sentRequests.length).toBe(1)
expect(sentRequests[0].payload.seqNo).toBe(0)
- isOnline = false
+ setOnline(false)
await OCPP20ServiceUtils.sendTransactionEvent(
mockChargingStation,
const connectorId = 1
const transactionId = generateUUID()
- isOnline = false
+ setOnline(false)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
const beforeQueue = new Date()
const connectorId = 1
const transactionId = generateUUID()
- isOnline = false
+ setOnline(false)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
await OCPP20ServiceUtils.sendTransactionEvent(
expect(sentRequests.length).toBe(0)
- isOnline = true
+ setOnline(true)
await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
const connectorId = 1
const transactionId = generateUUID()
- isOnline = false
+ setOnline(false)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
await OCPP20ServiceUtils.sendTransactionEvent(
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)
const connectorId = 1
const transactionId = generateUUID()
- isOnline = false
+ setOnline(false)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
await OCPP20ServiceUtils.sendTransactionEvent(
transactionId
)
- isOnline = true
+ setOnline(true)
await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
expect(sentRequests[0].payload.eventType).toBe(OCPP20TransactionEventEnumType.Started)
const connectorId = 1
const transactionId = generateUUID()
- isOnline = true
+ setOnline(true)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId)
await OCPP20ServiceUtils.sendTransactionEvent(
)
expect(sentRequests[0].payload.seqNo).toBe(0)
- isOnline = false
+ setOnline(false)
await OCPP20ServiceUtils.sendTransactionEvent(
mockChargingStation,
transactionId
)
- isOnline = true
+ setOnline(true)
await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, connectorId)
const transactionId1 = generateUUID()
const transactionId2 = generateUUID()
- isOnline = false
+ setOnline(false)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 1)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 2)
const transactionId1 = generateUUID()
const transactionId2 = generateUUID()
- isOnline = false
+ setOnline(false)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 1)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, 2)
transactionId2
)
- isOnline = true
+ setOnline(true)
await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockChargingStation, 1)
*/
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 {
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(() => {
+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'
// 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.
})
}
+/**
+ * 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.
*
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
import type { ChargingStation } from '../../../../../src/charging-station/ChargingStation.js'
import type {
AuthCache,
+ LocalAuthListManager,
OCPPAuthAdapter,
OCPPAuthService,
} from '../../../../../src/charging-station/ocpp/auth/interfaces/OCPPAuthService.js'
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,
+})
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
let mockOCPP20Adapter: OCPPAuthAdapter
beforeEach(() => {
- // Create mock charging station
mockChargingStation = {
logPrefix: () => '[TEST-CS-001]',
stationInfo: {
},
} 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)
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: {
})
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,
})
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,
})
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: {
})
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,
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: {
})
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: {
})
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: {
})
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,
})
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: {
})
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: {
})
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: {
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()
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 () => {
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)
})
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),
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),
})
})
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),
})
})
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,
})
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),
})
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 }
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 () => {
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)
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),
})
})
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,
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),
})
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),
})
})
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,
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),
})
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),
})
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),
})
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)
})
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)
})
})
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()
})
}
}
+/**
+ * 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
}
}
+/**
+ * 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
*
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()
+ }
+}
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(() => {
* - 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)
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', () => {
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(() => {
})
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)
// Clean up timeout
clearTimeout(timeout)
- } finally {
- t.mock.timers.reset()
- }
+ })
})
await it('should log info for success/termination codes, error for other codes', t => {