From: Jérôme Benoit Date: Sat, 28 Feb 2026 16:08:14 +0000 (+0100) Subject: fix(test-type-safety): remove unsafe type casts and add factory helper X-Git-Tag: ocpp-server@v3.0.0~84 X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=3d2431e2f36acc4768d14b7faccdf724fcd048a6;p=e-mobility-charging-stations-simulator.git fix(test-type-safety): remove unsafe type casts and add factory helper Phase 1 (Type Safety): - Remove 16 'undefined as unknown as' casts from 8 auth test files - Add createStationWithCertificateManager() helper to OCPP20TestUtils - Migrate 3 certificate test files to use the new type-safe helper Phase 2 (Enhancements): - Add explanatory comments to 9 ESLint override directives - Update TEST_STYLE_GUIDE.md with new factory documentation --- diff --git a/tests/TEST_STYLE_GUIDE.md b/tests/TEST_STYLE_GUIDE.md index 7450b2bd..db9bde70 100644 --- a/tests/TEST_STYLE_GUIDE.md +++ b/tests/TEST_STYLE_GUIDE.md @@ -272,17 +272,28 @@ const station = await createChargingStation({ The following utilities are available for reuse across test files: -| Utility | Location | Purpose | -| ----------------------------- | ------------------------------------ | ----------------------------------------- | -| `createMockChargingStation()` | `ChargingStationTestUtils.ts` | Lightweight mock station stub | -| `createChargingStation()` | `ChargingStationFactory.ts` | Full test station with OCPP services | -| `createConnectorStatus()` | `helpers/StationHelpers.ts` | ConnectorStatus factory with defaults | -| `MockWebSocket` | `mocks/MockWebSocket.ts` | WebSocket simulation with message capture | -| `MockIdTagsCache` | `mocks/MockCaches.ts` | In-memory IdTags cache mock | -| `MockSharedLRUCache` | `mocks/MockCaches.ts` | In-memory LRU cache mock | -| `waitForCondition()` | `helpers/StationHelpers.ts` | Async condition waiting with timeout | -| `cleanupChargingStation()` | `helpers/StationHelpers.ts` | Proper station cleanup for afterEach | -| Auth factories | `ocpp/auth/helpers/MockFactories.ts` | Auth-specific mock creation | +| Utility | Location | Purpose | +| --------------------------------------- | ------------------------------------ | -------------------------------------------- | +| `createMockChargingStation()` | `ChargingStationTestUtils.ts` | Lightweight mock station stub | +| `createChargingStation()` | `ChargingStationFactory.ts` | Full test station with OCPP services | +| `createConnectorStatus()` | `helpers/StationHelpers.ts` | ConnectorStatus factory with defaults | +| `createStationWithCertificateManager()` | `ocpp/2.0/OCPP20TestUtils.ts` | Station with certificate manager (type-safe) | +| `MockWebSocket` | `mocks/MockWebSocket.ts` | WebSocket simulation with message capture | +| `MockIdTagsCache` | `mocks/MockCaches.ts` | In-memory IdTags cache mock | +| `MockSharedLRUCache` | `mocks/MockCaches.ts` | In-memory LRU cache mock | +| `waitForCondition()` | `helpers/StationHelpers.ts` | Async condition waiting with timeout | +| `cleanupChargingStation()` | `helpers/StationHelpers.ts` | Proper station cleanup for afterEach | +| Auth factories | `ocpp/auth/helpers/MockFactories.ts` | Auth-specific mock creation | +| ----------------------------- | ------------------------------------ | ----------------------------------------- | +| `createMockChargingStation()` | `ChargingStationTestUtils.ts` | Lightweight mock station stub | +| `createChargingStation()` | `ChargingStationFactory.ts` | Full test station with OCPP services | +| `createConnectorStatus()` | `helpers/StationHelpers.ts` | ConnectorStatus factory with defaults | +| `MockWebSocket` | `mocks/MockWebSocket.ts` | WebSocket simulation with message capture | +| `MockIdTagsCache` | `mocks/MockCaches.ts` | In-memory IdTags cache mock | +| `MockSharedLRUCache` | `mocks/MockCaches.ts` | In-memory LRU cache mock | +| `waitForCondition()` | `helpers/StationHelpers.ts` | Async condition waiting with timeout | +| `cleanupChargingStation()` | `helpers/StationHelpers.ts` | Proper station cleanup for afterEach | +| Auth factories | `ocpp/auth/helpers/MockFactories.ts` | Auth-specific mock creation | **DO NOT duplicate these utilities.** Import and reuse them. diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-DeleteCertificate.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-DeleteCertificate.test.ts index 6138c821..d6c6b6ca 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-DeleteCertificate.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-DeleteCertificate.test.ts @@ -21,6 +21,7 @@ import { import { Constants } from '../../../../src/utils/index.js' import { createChargingStation } from '../../../ChargingStationFactory.js' import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js' +import { createStationWithCertificateManager } from './OCPP20TestUtils.js' const VALID_CERTIFICATE_HASH_DATA = { hashAlgorithm: HashAlgorithmEnumType.SHA256, @@ -76,9 +77,10 @@ await describe('I04 - DeleteCertificate', async () => { websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL, }) - // Cast to allow setting certificateManager property - stationWithCertManager = mockChargingStation as unknown as ChargingStationWithCertificateManager - stationWithCertManager.certificateManager = createMockCertificateManager() + stationWithCertManager = createStationWithCertificateManager( + mockChargingStation, + createMockCertificateManager() + ) incomingRequestService = new OCPP20IncomingRequestService() testableService = createTestableIncomingRequestService(incomingRequestService) diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetInstalledCertificateIds.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetInstalledCertificateIds.test.ts index c62a481f..a3f17ab4 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetInstalledCertificateIds.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetInstalledCertificateIds.test.ts @@ -23,6 +23,7 @@ import { import { Constants } from '../../../../src/utils/index.js' import { createChargingStation } from '../../../ChargingStationFactory.js' import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js' +import { createStationWithCertificateManager } from './OCPP20TestUtils.js' const createMockCertificateHashData = (serialNumber = '123456789'): CertificateHashDataType => ({ hashAlgorithm: HashAlgorithmEnumType.SHA256, @@ -77,9 +78,10 @@ await describe('I04 - GetInstalledCertificateIds', async () => { websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL, }) - // Cast to allow setting certificateManager property - stationWithCertManager = mockChargingStation as unknown as ChargingStationWithCertificateManager - stationWithCertManager.certificateManager = createMockCertificateManager() + stationWithCertManager = createStationWithCertificateManager( + mockChargingStation, + createMockCertificateManager() + ) incomingRequestService = new OCPP20IncomingRequestService() testableService = createTestableIncomingRequestService(incomingRequestService) diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-InstallCertificate.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-InstallCertificate.test.ts index 3e17f6df..f57460a0 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-InstallCertificate.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-InstallCertificate.test.ts @@ -20,6 +20,7 @@ import { import { Constants } from '../../../../src/utils/index.js' import { createChargingStation } from '../../../ChargingStationFactory.js' import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js' +import { createStationWithCertificateManager } from './OCPP20TestUtils.js' const VALID_PEM_CERTIFICATE = `-----BEGIN CERTIFICATE----- MIIBkTCB+wIJAKHBfpvPA0GXMA0GCSqGSIb3DQEBCwUAMBExDzANBgNVBAMMBnRl @@ -90,9 +91,11 @@ await describe('I03 - InstallCertificate', async () => { websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL, }) - // Cast to allow setting certificateManager property - stationWithCertManager = mockChargingStation as unknown as ChargingStationWithCertificateManager - stationWithCertManager.certificateManager = createMockCertificateManager() + // Use factory function to create station with certificate manager + stationWithCertManager = createStationWithCertificateManager( + mockChargingStation, + createMockCertificateManager() + ) incomingRequestService = new OCPP20IncomingRequestService() testableService = createTestableIncomingRequestService(incomingRequestService) diff --git a/tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts b/tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts index d41584ba..b447bd12 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts @@ -1,6 +1,7 @@ import { mock } from 'node:test' import type { ChargingStation } from '../../../../src/charging-station/ChargingStation.js' +import type { ChargingStationWithCertificateManager } from '../../../../src/charging-station/ocpp/2.0/OCPP20CertificateManager.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' @@ -629,3 +630,30 @@ export const TransactionFlowPatterns: TransactionFlowPattern[] = [ startContext: TransactionContextFixtures.remoteStart(), }, ] as const + +// ============================================================================ +// ChargingStationWithCertificateManager Factory +// ============================================================================ + +/** + * Create a mock ChargingStation with certificate manager for testing. + * This encapsulates the type casting pattern for ChargingStationWithCertificateManager. + * @param baseStation - Optional base station to extend (creates new if not provided) + * @param certificateManager - Certificate manager to attach + * @returns ChargingStation with certificateManager property properly typed + * @example + * ```typescript + * const station = createStationWithCertificateManager( + * createChargingStation({ ocppVersion: OCPPVersion.VERSION_201 }), + * createMockCertificateManager() + * ) + * ``` + */ +export function createStationWithCertificateManager ( + baseStation: ChargingStation, + certificateManager: ChargingStationWithCertificateManager['certificateManager'] +): ChargingStationWithCertificateManager { + const station = baseStation as ChargingStationWithCertificateManager + station.certificateManager = certificateManager + return station +} diff --git a/tests/charging-station/ocpp/auth/OCPPAuthIntegration.test.ts b/tests/charging-station/ocpp/auth/OCPPAuthIntegration.test.ts index ba91b4b1..8877e979 100644 --- a/tests/charging-station/ocpp/auth/OCPPAuthIntegration.test.ts +++ b/tests/charging-station/ocpp/auth/OCPPAuthIntegration.test.ts @@ -3,7 +3,7 @@ * @description Unit tests for OCPP authentication integration with deterministic mocked responses */ import { expect } from '@std/expect' -import { afterEach, beforeEach, describe, it } from 'node:test' +import { afterEach, beforeEach, describe, it, mock } from 'node:test' import type { ChargingStation } from '../../../../src/charging-station/ChargingStation.js' @@ -56,8 +56,7 @@ await describe('OCPP Authentication Integration Tests', async () => { }) afterEach(() => { - mockChargingStation16 = undefined as unknown as ChargingStation - mockChargingStation20 = undefined as unknown as ChargingStation + mock.reset() }) await describe('Service Initialization', async () => { diff --git a/tests/charging-station/ocpp/auth/adapters/OCPP16AuthAdapter.test.ts b/tests/charging-station/ocpp/auth/adapters/OCPP16AuthAdapter.test.ts index 419411f7..be24f97d 100644 --- a/tests/charging-station/ocpp/auth/adapters/OCPP16AuthAdapter.test.ts +++ b/tests/charging-station/ocpp/auth/adapters/OCPP16AuthAdapter.test.ts @@ -3,7 +3,7 @@ * @description Unit tests for OCPP 1.6 authentication adapter */ import { expect } from '@std/expect' -import { afterEach, beforeEach, describe, it } from 'node:test' +import { afterEach, beforeEach, describe, it, mock } from 'node:test' import type { ChargingStation } from '../../../../../src/charging-station/ChargingStation.js' import type { OCPP16AuthorizeResponse } from '../../../../../src/types/ocpp/1.6/Responses.js' @@ -57,8 +57,7 @@ await describe('OCPP16AuthAdapter', async () => { }) afterEach(() => { - adapter = undefined as unknown as OCPP16AuthAdapter - mockChargingStation = undefined as unknown as ChargingStation + mock.reset() }) await describe('constructor', async () => { diff --git a/tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter-Offline.test.ts b/tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter-Offline.test.ts index ec0dfd6d..679a52c2 100644 --- a/tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter-Offline.test.ts +++ b/tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter-Offline.test.ts @@ -17,7 +17,7 @@ */ import { expect } from '@std/expect' -import { afterEach, beforeEach, describe, it } from 'node:test' +import { afterEach, beforeEach, describe, it, mock } from 'node:test' import type { ChargingStation } from '../../../../../src/charging-station/ChargingStation.js' @@ -41,8 +41,7 @@ await describe('OCPP20AuthAdapter - G03.FR.02 Offline Authorization', async () = }) afterEach(() => { - adapter = undefined as unknown as OCPP20AuthAdapter - mockChargingStation = undefined as unknown as ChargingStation + mock.reset() }) await describe('G03.FR.02.001 - Offline detection', async () => { diff --git a/tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.test.ts b/tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.test.ts index 5c3b0e02..543df57c 100644 --- a/tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.test.ts +++ b/tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.test.ts @@ -3,7 +3,7 @@ * @description Unit tests for OCPP 2.0 authentication adapter */ import { expect } from '@std/expect' -import { afterEach, beforeEach, describe, it } from 'node:test' +import { afterEach, beforeEach, describe, it, mock } from 'node:test' import type { ChargingStation } from '../../../../../src/charging-station/ChargingStation.js' @@ -44,8 +44,7 @@ await describe('OCPP20AuthAdapter', async () => { }) afterEach(() => { - adapter = undefined as unknown as OCPP20AuthAdapter - mockChargingStation = undefined as unknown as ChargingStation + mock.reset() }) await describe('constructor', async () => { diff --git a/tests/charging-station/ocpp/auth/cache/InMemoryAuthCache.test.ts b/tests/charging-station/ocpp/auth/cache/InMemoryAuthCache.test.ts index 75e6ad10..247d1791 100644 --- a/tests/charging-station/ocpp/auth/cache/InMemoryAuthCache.test.ts +++ b/tests/charging-station/ocpp/auth/cache/InMemoryAuthCache.test.ts @@ -3,7 +3,7 @@ * @description Unit tests for in-memory authorization cache conformance (G03.FR.01) */ import { expect } from '@std/expect' -import { afterEach, beforeEach, describe, it } from 'node:test' +import { afterEach, beforeEach, describe, it, mock } from 'node:test' import { InMemoryAuthCache } from '../../../../../src/charging-station/ocpp/auth/cache/InMemoryAuthCache.js' import { @@ -39,7 +39,7 @@ await describe('InMemoryAuthCache - G03.FR.01 Conformance', async () => { }) afterEach(() => { - cache = undefined as unknown as InMemoryAuthCache + mock.reset() }) await describe('G03.FR.01.001 - Cache Hit Behavior', async () => { diff --git a/tests/charging-station/ocpp/auth/strategies/CertificateAuthStrategy.test.ts b/tests/charging-station/ocpp/auth/strategies/CertificateAuthStrategy.test.ts index da17e16e..9ce11570 100644 --- a/tests/charging-station/ocpp/auth/strategies/CertificateAuthStrategy.test.ts +++ b/tests/charging-station/ocpp/auth/strategies/CertificateAuthStrategy.test.ts @@ -3,7 +3,7 @@ * @description Unit tests for certificate-based authentication strategy */ import { expect } from '@std/expect' -import { afterEach, beforeEach, describe, it } from 'node:test' +import { afterEach, beforeEach, describe, it, mock } from 'node:test' import type { ChargingStation } from '../../../../../src/charging-station/ChargingStation.js' import type { OCPPAuthAdapter } from '../../../../../src/charging-station/ocpp/auth/interfaces/OCPPAuthService.js' @@ -57,8 +57,7 @@ await describe('CertificateAuthStrategy', async () => { }) afterEach(() => { - mockChargingStation = undefined as unknown as typeof mockChargingStation - mockOCPP20Adapter = undefined as unknown as typeof mockOCPP20Adapter + mock.reset() }) await describe('constructor', async () => { diff --git a/tests/charging-station/ocpp/auth/strategies/LocalAuthStrategy.test.ts b/tests/charging-station/ocpp/auth/strategies/LocalAuthStrategy.test.ts index 3701029f..0c5e0438 100644 --- a/tests/charging-station/ocpp/auth/strategies/LocalAuthStrategy.test.ts +++ b/tests/charging-station/ocpp/auth/strategies/LocalAuthStrategy.test.ts @@ -3,7 +3,7 @@ * @description Unit tests for local authorization strategy (cache and local list) */ import { expect } from '@std/expect' -import { afterEach, beforeEach, describe, it } from 'node:test' +import { afterEach, beforeEach, describe, it, mock } from 'node:test' import type { AuthCache, @@ -38,8 +38,7 @@ await describe('LocalAuthStrategy', async () => { }) afterEach(() => { - mockAuthCache = undefined as unknown as typeof mockAuthCache - mockLocalAuthListManager = undefined as unknown as typeof mockLocalAuthListManager + mock.reset() }) await describe('constructor', async () => { diff --git a/tests/charging-station/ocpp/auth/strategies/RemoteAuthStrategy.test.ts b/tests/charging-station/ocpp/auth/strategies/RemoteAuthStrategy.test.ts index 4a35c403..6f485b12 100644 --- a/tests/charging-station/ocpp/auth/strategies/RemoteAuthStrategy.test.ts +++ b/tests/charging-station/ocpp/auth/strategies/RemoteAuthStrategy.test.ts @@ -3,7 +3,7 @@ * @description Unit tests for remote (CSMS) authorization strategy */ import { expect } from '@std/expect' -import { afterEach, beforeEach, describe, it } from 'node:test' +import { afterEach, beforeEach, describe, it, mock } from 'node:test' import type { AuthCache, @@ -44,9 +44,7 @@ await describe('RemoteAuthStrategy', async () => { }) afterEach(() => { - mockAuthCache = undefined as unknown as typeof mockAuthCache - mockOCPP16Adapter = undefined as unknown as typeof mockOCPP16Adapter - mockOCPP20Adapter = undefined as unknown as typeof mockOCPP20Adapter + mock.reset() }) await describe('constructor', async () => { diff --git a/tests/charging-station/ocpp/auth/utils/AuthHelpers.test.ts b/tests/charging-station/ocpp/auth/utils/AuthHelpers.test.ts index e17dbd29..49706f98 100644 --- a/tests/charging-station/ocpp/auth/utils/AuthHelpers.test.ts +++ b/tests/charging-station/ocpp/auth/utils/AuthHelpers.test.ts @@ -210,7 +210,7 @@ await describe('AuthHelpers', async () => { }) await it('should return generic message for unknown status', () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any -- testing invalid status value expect(AuthHelpers.getStatusMessage('INVALID_STATUS' as any)).toBe('Authorization failed') }) }) diff --git a/tests/charging-station/ocpp/auth/utils/AuthValidators.test.ts b/tests/charging-station/ocpp/auth/utils/AuthValidators.test.ts index 5fbf2da5..5060b8f2 100644 --- a/tests/charging-station/ocpp/auth/utils/AuthValidators.test.ts +++ b/tests/charging-station/ocpp/auth/utils/AuthValidators.test.ts @@ -85,7 +85,7 @@ await describe('AuthValidators', async () => { }) await it('should return false for non-string input', () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any -- testing invalid type input expect(AuthValidators.isValidIdentifierValue(123 as any)).toBe(false) }) }) @@ -106,7 +106,7 @@ await describe('AuthValidators', async () => { }) await it('should return empty string for non-string input', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- testing invalid type input expect(AuthValidators.sanitizeIdTag(123 as any)).toBe('') }) @@ -133,7 +133,7 @@ await describe('AuthValidators', async () => { }) await it('should return empty string for non-string input', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- testing invalid type input expect(AuthValidators.sanitizeIdToken(123 as any)).toBe('') }) @@ -160,12 +160,12 @@ await describe('AuthValidators', async () => { }) await it('should return false for null configuration', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- testing null input expect(AuthValidators.validateAuthConfiguration(null as any)).toBe(false) }) await it('should return false for undefined configuration', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- testing undefined input expect(AuthValidators.validateAuthConfiguration(undefined as any)).toBe(false) }) @@ -240,7 +240,7 @@ await describe('AuthValidators', async () => { localAuthListEnabled: true, localPreAuthorize: true, offlineAuthorizationEnabled: false, - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- testing invalid enum value strategyPriorityOrder: ['InvalidMethod' as any], } @@ -269,12 +269,12 @@ await describe('AuthValidators', async () => { await describe('validateIdentifier', async () => { await it('should return false for undefined identifier', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- testing undefined input expect(AuthValidators.validateIdentifier(undefined as any)).toBe(false) }) await it('should return false for null identifier', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- testing null input expect(AuthValidators.validateIdentifier(null as any)).toBe(false) })