- Keep mocks focused - mock only what's necessary
- Verify mock calls when behavior depends on them
+## Test Isolation (CRITICAL)
+
+**NEVER define mock instances at module level inside describe blocks.** Each test must get fresh instances.
+
+❌ **Bad (Module-Level State Sharing):**
+
+```typescript
+await describe('My Test Suite', async () => {
+ afterEach(() => {
+ mock.restoreAll()
+ })
+
+ // WRONG: These instances are SHARED across all tests!
+ const mockResponseService = new OCPP20ResponseService()
+ const requestService = new OCPP20RequestService(mockResponseService)
+ const mockChargingStation = createChargingStation({...})
+
+ await it('test 1', () => { /* uses shared state */ })
+ await it('test 2', () => { /* uses same shared state! Test pollution risk! */ })
+})
+```
+
+✅ **Good (Fresh Instances Per Test):**
+
+```typescript
+await describe('My Test Suite', async () => {
+ let mockResponseService: OCPP20ResponseService
+ let requestService: OCPP20RequestService
+ let mockChargingStation: TestChargingStation
+
+ beforeEach(() => {
+ // Fresh instances for every test - proper isolation
+ mockResponseService = new OCPP20ResponseService()
+ requestService = new OCPP20RequestService(mockResponseService)
+ mockChargingStation = createChargingStation({...})
+ })
+
+ afterEach(() => {
+ mock.restoreAll()
+ })
+
+ await it('test 1', () => { /* clean state */ })
+ await it('test 2', () => { /* clean state */ })
+})
+```
+
+**Why:** Module-level state causes:
+
+- Test pollution (state leaks between tests)
+- Flaky tests (order-dependent results)
+- False positives/negatives
+- Difficult debugging
+
+**Exception:** Static constants (strings, numbers, frozen objects) CAN be at module level since they don't change.
+
## Cleanup Hooks
**ALWAYS include `afterEach()` cleanup to prevent test pollution:**
clearConnectorTransaction,
setupConnectorWithTransaction,
standardCleanup,
- TestEnvironmentHelper,
- TestStationHelper,
- TestTimerHelper,
} from '../helpers/TestLifecycleHelpers.js'
-export type { MockableTimerAPI, TimerHelperOptions } from '../helpers/TestLifecycleHelpers.js'
// Re-export all helper functions and types
export type {
cleanupChargingStation,
createConnectorStatus,
createMockChargingStation,
- createMockTemplate,
resetChargingStationState,
waitForCondition,
} from './helpers/StationHelpers.js'
import type { ChargingStation } from '../../../src/charging-station/ChargingStation.js'
import type {
- ChargingStationTemplate,
ConnectorStatus,
EvseStatus,
StopTransactionReason,
}
}
-/**
- * Create a mock template for testing
- * @param overrides - Template properties to override
- * @returns ChargingStationTemplate for testing
- */
-export function createMockTemplate (
- overrides: Partial<ChargingStationTemplate> = {}
-): ChargingStationTemplate {
- return {
- baseName: TEST_CHARGING_STATION_BASE_NAME,
- chargePointModel: 'Test Model',
- chargePointVendor: 'Test Vendor',
- numberOfConnectors: 2,
- ocppVersion: OCPPVersion.VERSION_16,
- ...overrides,
- } as ChargingStationTemplate
-}
-
/**
* Reset a ChargingStation to its initial state
*
* @description Unit tests for OCPP 2.0 BootNotification request building (B01)
*/
import { expect } from '@std/expect'
-import { afterEach, describe, it, mock } from 'node:test'
+import { afterEach, beforeEach, describe, it, mock } from 'node:test'
import { OCPP20RequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestService.js'
import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js'
} from '../../../../src/types/index.js'
import { type ChargingStationType } from '../../../../src/types/ocpp/2.0/Common.js'
import { Constants } from '../../../../src/utils/index.js'
-import { createChargingStation } from '../../../ChargingStationFactory.js'
+import { createChargingStation, type TestChargingStation } from '../../../ChargingStationFactory.js'
import {
TEST_CHARGE_POINT_MODEL,
TEST_CHARGE_POINT_SERIAL_NUMBER,
TEST_CHARGING_STATION_BASE_NAME,
TEST_FIRMWARE_VERSION,
} from '../../ChargingStationTestConstants.js'
-import { createTestableOCPP20RequestService } from './OCPP20TestUtils.js'
+import {
+ createTestableOCPP20RequestService,
+ type TestableOCPP20RequestService,
+} from './OCPP20TestUtils.js'
await describe('B01 - Cold Boot Charging Station', async () => {
- afterEach(() => {
- mock.restoreAll()
+ let mockResponseService: OCPP20ResponseService
+ let requestService: OCPP20RequestService
+ let testableRequestService: TestableOCPP20RequestService
+ let mockChargingStation: TestChargingStation
+
+ beforeEach(() => {
+ mockResponseService = new OCPP20ResponseService()
+ requestService = new OCPP20RequestService(mockResponseService)
+ testableRequestService = createTestableOCPP20RequestService(requestService)
+ mockChargingStation = createChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ chargePointModel: TEST_CHARGE_POINT_MODEL,
+ chargePointSerialNumber: TEST_CHARGE_POINT_SERIAL_NUMBER,
+ chargePointVendor: TEST_CHARGE_POINT_VENDOR,
+ firmwareVersion: TEST_FIRMWARE_VERSION,
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
})
- const mockResponseService = new OCPP20ResponseService()
- const requestService = new OCPP20RequestService(mockResponseService)
- const testableRequestService = createTestableOCPP20RequestService(requestService)
-
- const mockChargingStation = createChargingStation({
- baseName: TEST_CHARGING_STATION_BASE_NAME,
- connectorsCount: 3,
- evseConfiguration: { evsesCount: 3 },
- heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
- stationInfo: {
- chargePointModel: TEST_CHARGE_POINT_MODEL,
- chargePointSerialNumber: TEST_CHARGE_POINT_SERIAL_NUMBER,
- chargePointVendor: TEST_CHARGE_POINT_VENDOR,
- firmwareVersion: TEST_FIRMWARE_VERSION,
- ocppStrictCompliance: false,
- ocppVersion: OCPPVersion.VERSION_201,
- },
- websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ afterEach(() => {
+ mock.restoreAll()
})
// FR: B01.FR.01
* @description Unit tests for OCPP 2.0 Heartbeat request building (G02)
*/
import { expect } from '@std/expect'
-import { afterEach, describe, it, mock } from 'node:test'
+import { afterEach, beforeEach, describe, it, mock } from 'node:test'
import { OCPP20RequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestService.js'
import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js'
OCPPVersion,
} from '../../../../src/types/index.js'
import { Constants, has } from '../../../../src/utils/index.js'
-import { createChargingStation } from '../../../ChargingStationFactory.js'
+import { createChargingStation, type TestChargingStation } from '../../../ChargingStationFactory.js'
import {
TEST_CHARGE_POINT_MODEL,
TEST_CHARGE_POINT_SERIAL_NUMBER,
TEST_CHARGING_STATION_BASE_NAME,
TEST_FIRMWARE_VERSION,
} from '../../ChargingStationTestConstants.js'
-import { createTestableOCPP20RequestService } from './OCPP20TestUtils.js'
+import {
+ createTestableOCPP20RequestService,
+ type TestableOCPP20RequestService,
+} from './OCPP20TestUtils.js'
await describe('G02 - Heartbeat', async () => {
+ let mockResponseService: OCPP20ResponseService
+ let requestService: OCPP20RequestService
+ let testableRequestService: TestableOCPP20RequestService
+ let mockChargingStation: TestChargingStation
+
+ beforeEach(() => {
+ mockResponseService = new OCPP20ResponseService()
+ requestService = new OCPP20RequestService(mockResponseService)
+ testableRequestService = createTestableOCPP20RequestService(requestService)
+ mockChargingStation = createChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ chargePointModel: TEST_CHARGE_POINT_MODEL,
+ chargePointSerialNumber: TEST_CHARGE_POINT_SERIAL_NUMBER,
+ chargePointVendor: TEST_CHARGE_POINT_VENDOR,
+ firmwareVersion: TEST_FIRMWARE_VERSION,
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ })
+
afterEach(() => {
mock.restoreAll()
})
- const mockResponseService = new OCPP20ResponseService()
- const requestService = new OCPP20RequestService(mockResponseService)
- const testableRequestService = createTestableOCPP20RequestService(requestService)
-
- const mockChargingStation = createChargingStation({
- baseName: TEST_CHARGING_STATION_BASE_NAME,
- connectorsCount: 3,
- evseConfiguration: { evsesCount: 3 },
- heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
- stationInfo: {
- chargePointModel: TEST_CHARGE_POINT_MODEL,
- chargePointSerialNumber: TEST_CHARGE_POINT_SERIAL_NUMBER,
- chargePointVendor: TEST_CHARGE_POINT_VENDOR,
- firmwareVersion: TEST_FIRMWARE_VERSION,
- ocppStrictCompliance: false,
- ocppVersion: OCPPVersion.VERSION_201,
- },
- websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
- })
// FR: G02.FR.01
await it('should build HeartBeat request payload correctly with empty object', () => {
/* cspell:ignore Bvbn NQIF CBCYX */
import { expect } from '@std/expect'
-import { afterEach, describe, it, mock } from 'node:test'
+import { afterEach, beforeEach, describe, it, mock } from 'node:test'
import { createTestableRequestService } from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
import {
ReasonCodeEnumType,
} from '../../../../src/types/index.js'
import { Constants } from '../../../../src/utils/index.js'
-import { createChargingStation } from '../../../ChargingStationFactory.js'
+import { createChargingStation, type TestChargingStation } from '../../../ChargingStationFactory.js'
import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
// Sample Base64 EXI request (mock - represents CertificateInstallationReq)
})
await describe('M02 - Get15118EVCertificate Request', async () => {
+ let mockChargingStation: TestChargingStation
+
+ beforeEach(() => {
+ mockChargingStation = createChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ })
+
afterEach(() => {
mock.restoreAll()
})
- const mockChargingStation = createChargingStation({
- baseName: TEST_CHARGING_STATION_BASE_NAME,
- connectorsCount: 3,
- evseConfiguration: { evsesCount: 3 },
- heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
- stationInfo: {
- ocppStrictCompliance: false,
- ocppVersion: OCPPVersion.VERSION_201,
- },
- websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
- })
await describe('EXI Install Action', async () => {
await it('should forward EXI request unmodified for Install action', async () => {
})
await describe('M03 - GetCertificateStatus Request', async () => {
- const mockChargingStation = createChargingStation({
- baseName: TEST_CHARGING_STATION_BASE_NAME,
- connectorsCount: 3,
- evseConfiguration: { evsesCount: 3 },
- heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
- stationInfo: {
- ocppStrictCompliance: false,
- ocppVersion: OCPPVersion.VERSION_201,
- },
- websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ let mockChargingStation: TestChargingStation
+
+ beforeEach(() => {
+ mockChargingStation = createChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ })
+
+ afterEach(() => {
+ mock.restoreAll()
})
await describe('OCSP Request Data', async () => {
})
await describe('Request Command Names', async () => {
- const mockChargingStation = createChargingStation({
- baseName: TEST_CHARGING_STATION_BASE_NAME,
- connectorsCount: 1,
- evseConfiguration: { evsesCount: 1 },
- heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
- stationInfo: {
- ocppStrictCompliance: false,
- ocppVersion: OCPPVersion.VERSION_201,
- },
- websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ let mockChargingStation: TestChargingStation
+
+ beforeEach(() => {
+ mockChargingStation = createChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 1,
+ evseConfiguration: { evsesCount: 1 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ })
+
+ afterEach(() => {
+ mock.restoreAll()
})
await it('should send GET_15118_EV_CERTIFICATE command name', async () => {
*/
import { expect } from '@std/expect'
-import { afterEach, describe, it, mock } from 'node:test'
+import { afterEach, beforeEach, describe, it, mock } from 'node:test'
-import { createTestableRequestService } from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
+import {
+ createTestableRequestService,
+ type TestableOCPP20RequestService,
+} from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
import {
AttributeEnumType,
DataEnumType,
type ReportDataType,
} from '../../../../src/types/index.js'
import { Constants } from '../../../../src/utils/index.js'
-import { createChargingStation } from '../../../ChargingStationFactory.js'
+import { createChargingStation, type TestChargingStation } from '../../../ChargingStationFactory.js'
import {
TEST_CHARGE_POINT_MODEL,
TEST_CHARGE_POINT_SERIAL_NUMBER,
} from '../../ChargingStationTestConstants.js'
await describe('B07/B08 - NotifyReport', async () => {
+ let testableService: TestableOCPP20RequestService
+ let mockChargingStation: TestChargingStation
+
+ beforeEach(() => {
+ const { service } = createTestableRequestService()
+ testableService = service
+ mockChargingStation = createChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ chargePointModel: TEST_CHARGE_POINT_MODEL,
+ chargePointSerialNumber: TEST_CHARGE_POINT_SERIAL_NUMBER,
+ chargePointVendor: TEST_CHARGE_POINT_VENDOR,
+ firmwareVersion: TEST_FIRMWARE_VERSION,
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ })
+
afterEach(() => {
mock.restoreAll()
})
- const { service: testableService } = createTestableRequestService()
-
- const mockChargingStation = createChargingStation({
- baseName: TEST_CHARGING_STATION_BASE_NAME,
- connectorsCount: 3,
- evseConfiguration: { evsesCount: 3 },
- heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
- stationInfo: {
- chargePointModel: TEST_CHARGE_POINT_MODEL,
- chargePointSerialNumber: TEST_CHARGE_POINT_SERIAL_NUMBER,
- chargePointVendor: TEST_CHARGE_POINT_VENDOR,
- firmwareVersion: TEST_FIRMWARE_VERSION,
- ocppStrictCompliance: false,
- ocppVersion: OCPPVersion.VERSION_201,
- },
- websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
- })
// FR: B07.FR.03, B07.FR.04
await it('should build NotifyReport request payload correctly with minimal required fields', () => {
*/
import { expect } from '@std/expect'
-import { afterEach, describe, it, mock } from 'node:test'
+import { afterEach, beforeEach, describe, it, mock } from 'node:test'
import { createTestableRequestService } from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
import {
OCPPVersion,
} from '../../../../src/types/index.js'
import { Constants } from '../../../../src/utils/index.js'
-import { createChargingStation } from '../../../ChargingStationFactory.js'
+import { createChargingStation, type TestChargingStation } from '../../../ChargingStationFactory.js'
import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
const MOCK_ORGANIZATION_NAME = 'Test Organization Inc.'
await describe('I02 - SignCertificate Request', async () => {
+ let mockChargingStation: TestChargingStation
+
+ beforeEach(() => {
+ mockChargingStation = createChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ // Set up configuration with OrganizationName
+ mockChargingStation.ocppConfiguration = {
+ configurationKey: [{ key: 'SecurityCtrlr.OrganizationName', value: MOCK_ORGANIZATION_NAME }],
+ }
+ })
+
afterEach(() => {
mock.restoreAll()
})
- const mockChargingStation = createChargingStation({
- baseName: TEST_CHARGING_STATION_BASE_NAME,
- connectorsCount: 3,
- evseConfiguration: { evsesCount: 3 },
- heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
- stationInfo: {
- ocppStrictCompliance: false,
- ocppVersion: OCPPVersion.VERSION_201,
- },
- websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
- })
-
- // Set up configuration with OrganizationName
- mockChargingStation.ocppConfiguration = {
- configurationKey: [{ key: 'SecurityCtrlr.OrganizationName', value: MOCK_ORGANIZATION_NAME }],
- }
await describe('CSR Generation', async () => {
await it('should generate CSR with PKCS#10 PEM format', async () => {
* @description Unit tests for OCPP 2.0 StatusNotification request building (G01)
*/
import { expect } from '@std/expect'
-import { afterEach, describe, it, mock } from 'node:test'
+import { afterEach, beforeEach, describe, it, mock } from 'node:test'
import { OCPP20RequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestService.js'
import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js'
OCPPVersion,
} from '../../../../src/types/index.js'
import { Constants } from '../../../../src/utils/index.js'
-import { createChargingStation } from '../../../ChargingStationFactory.js'
+import { createChargingStation, type TestChargingStation } from '../../../ChargingStationFactory.js'
import {
TEST_FIRMWARE_VERSION,
TEST_STATUS_CHARGE_POINT_MODEL,
TEST_STATUS_CHARGE_POINT_VENDOR,
TEST_STATUS_CHARGING_STATION_BASE_NAME,
} from '../../ChargingStationTestConstants.js'
-import { createTestableOCPP20RequestService } from './OCPP20TestUtils.js'
+import {
+ createTestableOCPP20RequestService,
+ type TestableOCPP20RequestService,
+} from './OCPP20TestUtils.js'
await describe('G01 - Status Notification', async () => {
+ let mockResponseService: OCPP20ResponseService
+ let requestService: OCPP20RequestService
+ let testableRequestService: TestableOCPP20RequestService
+ let mockChargingStation: TestChargingStation
+
+ beforeEach(() => {
+ mockResponseService = new OCPP20ResponseService()
+ requestService = new OCPP20RequestService(mockResponseService)
+ testableRequestService = createTestableOCPP20RequestService(requestService)
+ mockChargingStation = createChargingStation({
+ baseName: TEST_STATUS_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ chargePointModel: TEST_STATUS_CHARGE_POINT_MODEL,
+ chargePointSerialNumber: TEST_STATUS_CHARGE_POINT_SERIAL_NUMBER,
+ chargePointVendor: TEST_STATUS_CHARGE_POINT_VENDOR,
+ firmwareVersion: TEST_FIRMWARE_VERSION,
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ })
+
afterEach(() => {
mock.restoreAll()
})
- const mockResponseService = new OCPP20ResponseService()
- const requestService = new OCPP20RequestService(mockResponseService)
- const testableRequestService = createTestableOCPP20RequestService(requestService)
-
- const mockChargingStation = createChargingStation({
- baseName: TEST_STATUS_CHARGING_STATION_BASE_NAME,
- connectorsCount: 3,
- evseConfiguration: { evsesCount: 3 },
- heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
- stationInfo: {
- chargePointModel: TEST_STATUS_CHARGE_POINT_MODEL,
- chargePointSerialNumber: TEST_STATUS_CHARGE_POINT_SERIAL_NUMBER,
- chargePointVendor: TEST_STATUS_CHARGE_POINT_VENDOR,
- firmwareVersion: TEST_FIRMWARE_VERSION,
- ocppStrictCompliance: false,
- ocppVersion: OCPPVersion.VERSION_201,
- },
- websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
- })
// FR: G01.FR.01
await it('should build StatusNotification request payload correctly with Available status', () => {
import { expect } from '@std/expect'
import type { ChargingStation } from '../../../../../src/charging-station/ChargingStation.js'
-import type { OCPPAuthService } from '../../../../../src/charging-station/ocpp/auth/interfaces/OCPPAuthService.js'
+import type {
+ AuthCache,
+ OCPPAuthAdapter,
+ OCPPAuthService,
+} from '../../../../../src/charging-station/ocpp/auth/interfaces/OCPPAuthService.js'
import {
type AuthConfiguration,
...overrides,
}) as OCPPAuthService
+// ============================================================================
+// Cache Mocks
+// ============================================================================
+
+/**
+ * Create a mock AuthCache for testing.
+ * @param overrides - Partial AuthCache methods to override defaults
+ * @returns Mock AuthCache with stubbed async methods
+ */
+export const createMockAuthCache = (overrides?: Partial<AuthCache>): AuthCache => ({
+ clear: async () => Promise.resolve(),
+ get: async (_key: string) => Promise.resolve(undefined),
+ getStats: async () =>
+ Promise.resolve({
+ evictions: 0,
+ expiredEntries: 0,
+ hitRate: 0,
+ hits: 0,
+ memoryUsage: 0,
+ misses: 0,
+ totalEntries: 0,
+ }),
+ remove: async (_key: string) => Promise.resolve(),
+ set: async (_key: string, _value: unknown, _ttl?: number) => Promise.resolve(),
+ ...overrides,
+})
+
+// ============================================================================
+// Adapter Mocks
+// ============================================================================
+
+/**
+ * Create a mock OCPPAuthAdapter for testing.
+ * @param ocppVersion - OCPP version for this adapter
+ * @param overrides - Partial OCPPAuthAdapter methods to override defaults
+ * @returns Mock OCPPAuthAdapter with stubbed methods
+ */
+export const createMockOCPPAdapter = (
+ ocppVersion: OCPPVersion,
+ overrides?: Partial<OCPPAuthAdapter>
+): OCPPAuthAdapter => ({
+ authorizeRemote: async (_identifier: UnifiedIdentifier) =>
+ Promise.resolve(
+ createMockAuthorizationResult({
+ method: AuthenticationMethod.REMOTE_AUTHORIZATION,
+ })
+ ),
+ convertFromUnifiedIdentifier: (identifier: UnifiedIdentifier) =>
+ ocppVersion === OCPPVersion.VERSION_16
+ ? identifier.value
+ : { idToken: identifier.value, type: identifier.type },
+ convertToUnifiedIdentifier: (identifier: object | string) => ({
+ ocppVersion,
+ type: IdentifierType.ID_TAG,
+ value:
+ typeof identifier === 'string'
+ ? identifier
+ : ((identifier as { idToken?: string }).idToken ?? 'unknown'),
+ }),
+ getConfigurationSchema: () => ({}),
+ isRemoteAvailable: async () => Promise.resolve(true),
+ ocppVersion,
+ validateConfiguration: async (_config: AuthConfiguration) => Promise.resolve(true),
+ ...overrides,
+})
// ============================================================================
// Assertion Helpers
// ============================================================================
import { mock } from 'node:test'
import type { ChargingStation } from '../../src/charging-station/ChargingStation.js'
-import type {
- MockChargingStationOptions,
- MockChargingStationResult,
-} from '../charging-station/helpers/StationHelpers.js'
-import {
- cleanupChargingStation,
- createMockChargingStation,
-} from '../charging-station/helpers/StationHelpers.js'
import { MockIdTagsCache, MockSharedLRUCache } from '../charging-station/mocks/MockCaches.js'
/**
}
}
-/**
- * Helper class for managing mock charging stations in tests
- *
- * Provides automatic cleanup of charging station resources including
- * timers, WebSocket connections, and singleton mocks.
- * @example
- * ```typescript
- * describe('MyTest', () => {
- * const stationHelper = new TestStationHelper({ connectorsCount: 2 })
- *
- * beforeEach(() => stationHelper.setup())
- * afterEach(() => stationHelper.cleanup())
- *
- * it('should test station', () => {
- * const { station, mocks } = stationHelper.get()
- * // test with station
- * })
- * })
- * ```
- */
-export class TestStationHelper {
- private readonly options: MockChargingStationOptions
- private result: MockChargingStationResult | null = null
-
- constructor (options: MockChargingStationOptions = {}) {
- this.options = options
- }
-
- /**
- * Clean up the charging station (call in afterEach)
- */
- cleanup (): void {
- if (this.result != null) {
- cleanupChargingStation(this.result.station)
- this.result = null
- }
- // Also reset singleton mocks
- MockSharedLRUCache.resetInstance()
- MockIdTagsCache.resetInstance()
- }
-
- /**
- * Get the current mock result (throws if not setup)
- * @returns The mock charging station result
- */
- get (): MockChargingStationResult {
- if (this.result == null) {
- throw new Error('TestStationHelper.setup() must be called before get()')
- }
- return this.result
- }
-
- /**
- * Get just the station (convenience method)
- * @returns The charging station instance
- */
- getStation (): ChargingStation {
- return this.get().station
- }
-
- /**
- * Check if station is currently setup
- * @returns True if station is setup
- */
- isSetup (): boolean {
- return this.result != null
- }
-
- /**
- * Create the mock charging station (call in beforeEach)
- * @returns The mock charging station result
- */
- setup (): MockChargingStationResult {
- this.result = createMockChargingStation(this.options)
- return this.result
- }
-}
-
-/**
- * Helper class for managing mock timers in tests
- *
- * Encapsulates the common pattern of enabling and resetting mock timers,
- * ensuring consistent cleanup and preventing timer leaks between tests.
- * @example
- * ```typescript
- * describe('MyTest', () => {
- * const timerHelper = new TestTimerHelper()
- *
- * beforeEach(() => timerHelper.setup())
- * afterEach(() => timerHelper.cleanup())
- *
- * it('should handle timer-based logic', () => {
- * // Timer-dependent code works with mock timers
- * mock.timers.tick(1000)
- * })
- * })
- * ```
- */
-export class TestTimerHelper {
- private readonly apis: MockableTimerAPI[]
- private isSetup = false
-
- constructor (options: TimerHelperOptions = {}) {
- this.apis = options.apis ?? ['setInterval', 'setTimeout', 'setImmediate']
- }
-
- /**
- * Reset mock timers (call in afterEach)
- */
- cleanup (): void {
- if (this.isSetup) {
- mock.timers.reset()
- this.isSetup = false
- }
- }
-
- /**
- * Enable mock timers (call in beforeEach)
- */
- setup (): void {
- if (!this.isSetup) {
- mock.timers.enable({ apis: this.apis })
- this.isSetup = true
- }
- }
-
- /**
- * Advance mock timers by specified milliseconds
- * @param ms - Milliseconds to advance
- */
- tick (ms: number): void {
- mock.timers.tick(ms)
- }
-}
-
-/**
- * Combined helper for tests that need both timers and station
- * @example
- * ```typescript
- * describe('MyTest', () => {
- * const helper = new TestEnvironmentHelper({ connectorsCount: 2 })
- *
- * beforeEach(() => helper.setup())
- * afterEach(() => helper.cleanup())
- *
- * it('should test with timers and station', () => {
- * const station = helper.getStation()
- * helper.tick(1000) // Advance time
- * })
- * })
- * ```
- */
-export class TestEnvironmentHelper {
- private readonly stationHelper: TestStationHelper
- private readonly timerHelper: TestTimerHelper
-
- constructor (
- stationOptions: MockChargingStationOptions = {},
- timerOptions: TimerHelperOptions = {}
- ) {
- this.timerHelper = new TestTimerHelper(timerOptions)
- this.stationHelper = new TestStationHelper(stationOptions)
- }
-
- /**
- * Cleanup both timers and station
- */
- cleanup (): void {
- this.stationHelper.cleanup()
- this.timerHelper.cleanup()
- mock.restoreAll()
- }
-
- /**
- * Get the mock station result
- * @returns The mock charging station result
- */
- get (): MockChargingStationResult {
- return this.stationHelper.get()
- }
-
- /**
- * Get just the station
- * @returns The charging station instance
- */
- getStation (): ChargingStation {
- return this.stationHelper.getStation()
- }
-
- /**
- * Setup both timers and station
- * @returns The mock charging station result
- */
- setup (): MockChargingStationResult {
- this.timerHelper.setup()
- return this.stationHelper.setup()
- }
-
- /**
- * Advance mock timers
- * @param ms - Milliseconds to advance
- */
- tick (ms: number): void {
- this.timerHelper.tick(ms)
- }
-}
-
/**
* Clear transaction state from a connector
* @param station - ChargingStation instance