]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
fix(test-type-safety): remove unsafe type casts and add factory helper
authorJérôme Benoit <jerome.benoit@sap.com>
Sat, 28 Feb 2026 16:08:14 +0000 (17:08 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Sat, 28 Feb 2026 16:08:14 +0000 (17:08 +0100)
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

15 files changed:
tests/TEST_STYLE_GUIDE.md
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-DeleteCertificate.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetInstalledCertificateIds.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-InstallCertificate.test.ts
tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts
tests/charging-station/ocpp/auth/OCPPAuthIntegration.test.ts
tests/charging-station/ocpp/auth/adapters/OCPP16AuthAdapter.test.ts
tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter-Offline.test.ts
tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.test.ts
tests/charging-station/ocpp/auth/cache/InMemoryAuthCache.test.ts
tests/charging-station/ocpp/auth/strategies/CertificateAuthStrategy.test.ts
tests/charging-station/ocpp/auth/strategies/LocalAuthStrategy.test.ts
tests/charging-station/ocpp/auth/strategies/RemoteAuthStrategy.test.ts
tests/charging-station/ocpp/auth/utils/AuthHelpers.test.ts
tests/charging-station/ocpp/auth/utils/AuthValidators.test.ts

index 7450b2bd4f24ca6fd62a6dc67511a01e2135c316..db9bde70f241574b5c628dfe0cb9e3e6c77d5e88 100644 (file)
@@ -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.
 
index 6138c821b0fb158eda53bb1747b21c481a5a09da..d6c6b6ca70f1ddb99f4f7cff5e838447cefea765 100644 (file)
@@ -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)
index c62a481f343434e4389b59d1f10759620193526d..a3f17ab446a0c2a3f786268900b699b890f254f0 100644 (file)
@@ -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)
index 3e17f6df1fdd57e3ece7914b5678cc79e9f80aa6..f57460a0157e7602a4d99ba9c86ee26d86d03c53 100644 (file)
@@ -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)
index d41584ba53379e0549b93adea1045b453ba4e188..b447bd12c0bd22f8afc6c500704bf9f08cdf5b0d 100644 (file)
@@ -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
+}
index ba91b4b143914aa445b8664ba9506ef7b7a3ae5e..8877e979a4ec3908b02ae9d71e45a57b38f5cc30 100644 (file)
@@ -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 () => {
index 419411f735ac3b9a772b510586fc9a0a4b74ddae..be24f97d03bf941858eed0a45f6d661d67b9d5b7 100644 (file)
@@ -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 () => {
index ec0dfd6d7064525562e28a563ae175449237b279..679a52c22c85a820f2aa914a9388c108cf1f4777 100644 (file)
@@ -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 () => {
index 5c3b0e02b253b0d9d579bdea37e8279fbab1c670..543df57c43a43a847042acc8167075d19833ab93 100644 (file)
@@ -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 () => {
index 75e6ad101ebe0affea24099b260fc1bef986cb58..247d17910db5c197757a093a70186ec62ea3fe7c 100644 (file)
@@ -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 () => {
index da17e16ebdb2f0bd3ebb648967342b77fe71e156..9ce11570cb12806b687c57e4445180aa7df49f63 100644 (file)
@@ -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 () => {
index 3701029ff049aafa6ad96c7cc8e9f1f843f8815a..0c5e0438e98c09df918bf6ebcabe91b40ce6a53f 100644 (file)
@@ -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 () => {
index 4a35c40323f4183c0d8b04896d500405f722aaa3..6f485b12ec4dfd5d5cbed35c5da876711c336833 100644 (file)
@@ -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 () => {
index e17dbd29e3be5805f3da663c6c67578a4d5912a8..49706f98c3a4cd9171c92656a0f5e2b72db40b07 100644 (file)
@@ -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')
     })
   })
index 5fbf2da592da6cf128ed2e611de11eb7f442b328..5060b8f29a520c1cb0427621558e25a65416e171 100644 (file)
@@ -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)
     })