From cac380fc8eb8f07bddd63f01d474888d3210267e Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 27 Feb 2026 23:54:59 +0100 Subject: [PATCH] refactor(tests): improve test isolation and remove dead code - Fix module-level state sharing in 6 OCPP 2.0 RequestService tests - Remove unused createMockTemplate function from StationHelpers - Remove unused TestStationHelper, TestTimerHelper, TestEnvironmentHelper classes (206 LOC) - Add createMockAuthCache and createMockOCPPAdapter factories - Update TEST_STYLE_GUIDE.md with test isolation best practices - Clean up orphaned imports after dead code removal --- tests/TEST_STYLE_GUIDE.md | 55 +++++ .../ChargingStationTestUtils.ts | 5 - .../helpers/StationHelpers.ts | 19 -- ...P20RequestService-BootNotification.test.ts | 55 +++-- .../OCPP20RequestService-HeartBeat.test.ts | 54 +++-- .../2.0/OCPP20RequestService-ISO15118.test.ts | 87 ++++--- .../OCPP20RequestService-NotifyReport.test.ts | 49 ++-- ...PP20RequestService-SignCertificate.test.ts | 40 ++-- ...0RequestService-StatusNotification.test.ts | 54 +++-- .../ocpp/auth/helpers/MockFactories.ts | 71 +++++- tests/helpers/TestLifecycleHelpers.ts | 215 ------------------ 11 files changed, 326 insertions(+), 378 deletions(-) diff --git a/tests/TEST_STYLE_GUIDE.md b/tests/TEST_STYLE_GUIDE.md index 903ce704..2fc517bf 100644 --- a/tests/TEST_STYLE_GUIDE.md +++ b/tests/TEST_STYLE_GUIDE.md @@ -164,6 +164,61 @@ import { waitForCondition } from './helpers/StationHelpers.js' - 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:** diff --git a/tests/charging-station/ChargingStationTestUtils.ts b/tests/charging-station/ChargingStationTestUtils.ts index 66c697bc..e53ae1ab 100644 --- a/tests/charging-station/ChargingStationTestUtils.ts +++ b/tests/charging-station/ChargingStationTestUtils.ts @@ -17,11 +17,7 @@ export { 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 { @@ -35,7 +31,6 @@ export { cleanupChargingStation, createConnectorStatus, createMockChargingStation, - createMockTemplate, resetChargingStationState, waitForCondition, } from './helpers/StationHelpers.js' diff --git a/tests/charging-station/helpers/StationHelpers.ts b/tests/charging-station/helpers/StationHelpers.ts index 68c1d7d5..2dcd49ef 100644 --- a/tests/charging-station/helpers/StationHelpers.ts +++ b/tests/charging-station/helpers/StationHelpers.ts @@ -6,7 +6,6 @@ import type { ChargingStation } from '../../../src/charging-station/ChargingStation.js' import type { - ChargingStationTemplate, ConnectorStatus, EvseStatus, StopTransactionReason, @@ -747,24 +746,6 @@ export function createMockChargingStation ( } } -/** - * Create a mock template for testing - * @param overrides - Template properties to override - * @returns ChargingStationTemplate for testing - */ -export function createMockTemplate ( - overrides: Partial = {} -): 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 * diff --git a/tests/charging-station/ocpp/2.0/OCPP20RequestService-BootNotification.test.ts b/tests/charging-station/ocpp/2.0/OCPP20RequestService-BootNotification.test.ts index 2e24dfd4..da9835a1 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20RequestService-BootNotification.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20RequestService-BootNotification.test.ts @@ -3,7 +3,7 @@ * @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' @@ -15,7 +15,7 @@ import { } 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, @@ -23,31 +23,40 @@ import { 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 diff --git a/tests/charging-station/ocpp/2.0/OCPP20RequestService-HeartBeat.test.ts b/tests/charging-station/ocpp/2.0/OCPP20RequestService-HeartBeat.test.ts index 02d0d83a..cc4a119a 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20RequestService-HeartBeat.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20RequestService-HeartBeat.test.ts @@ -3,7 +3,7 @@ * @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' @@ -13,7 +13,7 @@ import { 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, @@ -21,31 +21,41 @@ import { 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', () => { diff --git a/tests/charging-station/ocpp/2.0/OCPP20RequestService-ISO15118.test.ts b/tests/charging-station/ocpp/2.0/OCPP20RequestService-ISO15118.test.ts index 506ad930..3836e607 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20RequestService-ISO15118.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20RequestService-ISO15118.test.ts @@ -5,7 +5,7 @@ /* 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 { @@ -23,7 +23,7 @@ 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) @@ -42,20 +42,25 @@ const createMockOCSPRequestData = (): OCSPRequestDataType => ({ }) 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 () => { @@ -204,16 +209,24 @@ await describe('M02 - Get15118EVCertificate Request', 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 () => { @@ -313,16 +326,24 @@ await describe('M03 - GetCertificateStatus Request', 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 () => { diff --git a/tests/charging-station/ocpp/2.0/OCPP20RequestService-NotifyReport.test.ts b/tests/charging-station/ocpp/2.0/OCPP20RequestService-NotifyReport.test.ts index 6b55e969..82cbe448 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20RequestService-NotifyReport.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20RequestService-NotifyReport.test.ts @@ -4,9 +4,12 @@ */ 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, @@ -19,7 +22,7 @@ import { 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, @@ -29,26 +32,32 @@ import { } 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', () => { diff --git a/tests/charging-station/ocpp/2.0/OCPP20RequestService-SignCertificate.test.ts b/tests/charging-station/ocpp/2.0/OCPP20RequestService-SignCertificate.test.ts index 67532861..d3e718c8 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20RequestService-SignCertificate.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20RequestService-SignCertificate.test.ts @@ -4,7 +4,7 @@ */ 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 { @@ -16,31 +16,35 @@ 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 () => { diff --git a/tests/charging-station/ocpp/2.0/OCPP20RequestService-StatusNotification.test.ts b/tests/charging-station/ocpp/2.0/OCPP20RequestService-StatusNotification.test.ts index 49de1404..c4a6bdb2 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20RequestService-StatusNotification.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20RequestService-StatusNotification.test.ts @@ -3,7 +3,7 @@ * @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' @@ -14,7 +14,7 @@ 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_FIRMWARE_VERSION, TEST_STATUS_CHARGE_POINT_MODEL, @@ -22,31 +22,41 @@ import { 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', () => { diff --git a/tests/charging-station/ocpp/auth/helpers/MockFactories.ts b/tests/charging-station/ocpp/auth/helpers/MockFactories.ts index aecf8905..bb9f7ec4 100644 --- a/tests/charging-station/ocpp/auth/helpers/MockFactories.ts +++ b/tests/charging-station/ocpp/auth/helpers/MockFactories.ts @@ -3,7 +3,11 @@ 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, @@ -178,6 +182,71 @@ export const createMockAuthService = (overrides?: Partial): OCP ...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 => ({ + 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 => ({ + 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 // ============================================================================ diff --git a/tests/helpers/TestLifecycleHelpers.ts b/tests/helpers/TestLifecycleHelpers.ts index 4de31796..da06d23c 100644 --- a/tests/helpers/TestLifecycleHelpers.ts +++ b/tests/helpers/TestLifecycleHelpers.ts @@ -28,15 +28,7 @@ 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' /** @@ -80,213 +72,6 @@ interface MockContext { } } -/** - * 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 -- 2.53.0