]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
test: use shared response test helpers (#1930)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Mon, 29 Jun 2026 18:24:09 +0000 (20:24 +0200)
committerGitHub <noreply@github.com>
Mon, 29 Jun 2026 18:24:09 +0000 (20:24 +0200)
* test(ui-server): use response drain helpers

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* test(ocpp): share response service test helpers

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* test(ocpp): use auth factory test instances

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* refactor(ocpp): move ResponseService testable wrapper to __testable__

Aligns OCPP 2.0 ResponseService test wiring with the established pattern used
by RequestService and VariableManager:

- New src/charging-station/ocpp/2.0/__testable__/OCPP20ResponseServiceTestable.ts
  exporting createTestableResponseService + TestableOCPP20ResponseService;
  mirrors OCPP20RequestServiceTestable.ts structure (function declarations,
  file-level JSDoc with @example, double-cast through unknown).
- __testable__/index.ts re-exports the new module.
- OCPP20ResponseService-TransactionEvent.test.ts and -ForceTxOnInvalid.test.ts
  now import createTestableResponseService from __testable__/index.js;
  call sites renamed (drops redundant OCPP20 prefix, consistent with
  createTestableRequestService).
- OCPP20ResponseServiceTestUtils.ts now contains only the test-only
  buildTransactionEventRequest payload builder; adopts @file header and
  arrow-style export to harmonize with MockFactories.ts neighbour.

Test-only refactor; no production behavior change.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* test(ocpp): factor auth service injection helpers

Centralizes the two patterns used by OCPP 2.0 IncomingRequest tests to
inject mock OCPPAuthService instances into the factory cache:

- injectMockAuthService(station, service) — wraps the station-id lookup +
  OCPPAuthServiceFactory.setInstanceForTesting plumbing; replaces the
  duplicated boilerplate in ClearCache.test.ts and LocalAuthList.test.ts.
- withThrowingAuthServiceFactory(errorMessage, fn) — scoped monkey-patch +
  try/finally restore for the 3 sites that must exercise a failing
  getInstance (factory unavailable, no Authorization Cache support).
  Required because setInstanceForTesting only injects a returned instance;
  it cannot make getInstance itself throw.

Side effect of the helper adoption:

- LocalAuthList.test.ts drops the suite-scoped originalGetInstance save
  and afterEach restore — no longer needed since every monkey-patch is
  now scoped to its own test via withThrowingAuthServiceFactory.
- ClearCache.test.ts drops the local try/finally on the no-cache-support
  test for the same reason.

Test-only refactor; no production behavior change.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* test(ocpp): align auth mock helper verbs and drop dead return type

V2-1: harmonize JSDoc verbs across the auth injection helpers — `inject*`
docstrings now start with 'Inject' to match the function name, and the local
`setupMockAuthService` wrappers (which build the mock + inject it) now start
with 'Configure and inject' to reflect their broader role. Avoids the verb
collision between the shared `injectMockAuthService` and the local
`setupMockAuthService` helpers.

V2-2: drop the dead `OCPPAuthService` return type from LocalAuthList's local
`setupMockAuthService` — no call site captured the return value. Also drops
the now-unused `OCPPAuthService` type import.

Test-only refactor; no production behavior change.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* test(ocpp): align ClearCache test hook ordering with siblings

Moves the `afterEach` block below `beforeEach` in ClearCache.test.ts to match
the before-then-after ordering used by LocalAuthList, TransactionEvent and
ForceTxOnInvalid test files. Cosmetic; no behavior change.

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
---------

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
src/charging-station/ocpp/2.0/__testable__/OCPP20ResponseServiceTestable.ts [new file with mode: 0644]
src/charging-station/ocpp/2.0/__testable__/index.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ClearCache.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-LocalAuthList.test.ts
tests/charging-station/ocpp/2.0/OCPP20ResponseService-ForceTxOnInvalid.test.ts
tests/charging-station/ocpp/2.0/OCPP20ResponseService-TransactionEvent.test.ts
tests/charging-station/ocpp/2.0/OCPP20ResponseServiceTestUtils.ts [new file with mode: 0644]
tests/charging-station/ocpp/auth/helpers/MockFactories.ts
tests/charging-station/ui-server/UIHttpServer.test.ts
tests/charging-station/ui-server/UIMetricsEndpoint.test.ts

diff --git a/src/charging-station/ocpp/2.0/__testable__/OCPP20ResponseServiceTestable.ts b/src/charging-station/ocpp/2.0/__testable__/OCPP20ResponseServiceTestable.ts
new file mode 100644 (file)
index 0000000..36d7baa
--- /dev/null
@@ -0,0 +1,63 @@
+/**
+ * Testable wrapper for OCPP 2.0 ResponseService
+ *
+ * This module provides type-safe access to private handler methods of
+ * OCPP20ResponseService for testing purposes. It replaces ad hoc
+ * `as unknown as` casts at test sites with a properly typed interface,
+ * enabling:
+ * - Type-safe method invocations in tests
+ * - IntelliSense and autocompletion for handler parameters and returns
+ * - Compile-time checking for test code
+ * @example
+ * ```typescript
+ * import { createTestableResponseService } from './__testable__/index.js'
+ *
+ * const testable = createTestableResponseService(new OCPP20ResponseService())
+ * await testable.handleResponseTransactionEvent(station, payload, requestPayload)
+ * ```
+ */
+
+import type {
+  OCPP20TransactionEventRequest,
+  OCPP20TransactionEventResponse,
+} from '../../../../types/index.js'
+import type { ChargingStation } from '../../../index.js'
+import type { OCPP20ResponseService } from '../OCPP20ResponseService.js'
+
+/**
+ * Interface exposing private handler methods of OCPP20ResponseService for testing.
+ * Each method signature matches the corresponding private method in the service class.
+ */
+export interface TestableOCPP20ResponseService {
+  handleResponseTransactionEvent: (
+    chargingStation: ChargingStation,
+    payload: OCPP20TransactionEventResponse,
+    requestPayload: OCPP20TransactionEventRequest
+  ) => Promise<void>
+}
+
+/**
+ * Creates a testable wrapper around OCPP20ResponseService.
+ * Provides type-safe access to private handler methods without `as any` casts.
+ * @param service - The OCPP20ResponseService instance to wrap
+ * @returns A typed interface exposing private handler methods
+ * @example
+ * ```typescript
+ * // Before (with as any cast):
+ * await (service as any).handleResponseTransactionEvent(station, payload, requestPayload)
+ *
+ * // After (with testable interface):
+ * const testable = createTestableResponseService(service)
+ * await testable.handleResponseTransactionEvent(station, payload, requestPayload)
+ * ```
+ */
+export function createTestableResponseService (
+  service: OCPP20ResponseService
+): TestableOCPP20ResponseService {
+  // Cast to unknown first to satisfy TypeScript while preserving runtime behavior
+  const serviceImpl = service as unknown as TestableOCPP20ResponseService
+
+  return {
+    handleResponseTransactionEvent: serviceImpl.handleResponseTransactionEvent.bind(service),
+  }
+}
index c3bd89cb6e294f32d5083ef00a0db1b0e7426c8b..40629a6f3ef4f1b64168e1d8597712e5ba773d72 100644 (file)
@@ -320,6 +320,11 @@ export {
   type TestableRequestServiceResult,
 } from './OCPP20RequestServiceTestable.js'
 
+export {
+  createTestableResponseService,
+  type TestableOCPP20ResponseService,
+} from './OCPP20ResponseServiceTestable.js'
+
 export {
   createTestableVariableManager,
   type TestableOCPP20VariableManager,
index 2c77a527a356914400a8582e43a3a800d2fb2b2a..13fe1d6bdd024f20ead2461de92b50748066f883 100644 (file)
@@ -16,12 +16,36 @@ import { Constants } from '../../../../src/utils/index.js'
 import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
 import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
 import { createMockChargingStation } from '../../helpers/StationHelpers.js'
+import {
+  createMockAuthService,
+  createTestAuthConfig,
+  injectMockAuthService,
+  withThrowingAuthServiceFactory,
+} from '../auth/helpers/MockFactories.js'
 
-await describe('C11 - Clear Authorization Data in Authorization Cache', async () => {
-  afterEach(() => {
-    standardCleanup()
-  })
+/**
+ * Configure and inject a mock auth service for the current station.
+ * @param station - Charging station under test
+ * @param authorizationCacheEnabled - Whether the mock auth cache is enabled
+ * @param clearCache - Mock cache clearing implementation
+ */
+function setupMockAuthService (
+  station: ChargingStation,
+  authorizationCacheEnabled: boolean,
+  clearCache: () => void = () => {
+    /* empty */
+  }
+): void {
+  injectMockAuthService(
+    station,
+    createMockAuthService({
+      clearCache,
+      getConfiguration: () => createTestAuthConfig({ authorizationCacheEnabled }),
+    })
+  )
+}
 
+await describe('C11 - Clear Authorization Data in Authorization Cache', async () => {
   let station: ChargingStation
   let incomingRequestService: OCPP20IncomingRequestService
   let testableService: ReturnType<typeof createTestableIncomingRequestService>
@@ -42,6 +66,11 @@ await describe('C11 - Clear Authorization Data in Authorization Cache', async ()
     testableService = createTestableIncomingRequestService(incomingRequestService)
   })
 
+  afterEach(() => {
+    OCPPAuthServiceFactory.clearAllInstances()
+    standardCleanup()
+  })
+
   // FR: C11.FR.01 - CS SHALL attempt to clear its Authorization Cache
   await it('should handle ClearCache request successfully', async () => {
     const response = await testableService.handleRequestClearCache(station)
@@ -66,30 +95,13 @@ await describe('C11 - Clear Authorization Data in Authorization Cache', async ()
   // CLR-001: Verify Authorization Cache is cleared (not IdTagsCache)
   await describe('CLR-001 - ClearCache clears Authorization Cache', async () => {
     await it('should call authService.clearCache() on ClearCache request', async () => {
-      // Create a mock auth service to verify clearCache is called
       const clearCacheMock = mock.fn()
-      const mockAuthService = {
-        clearCache: clearCacheMock,
-        getConfiguration: () => ({
-          authorizationCacheEnabled: true,
-        }),
-      }
-
-      // Mock the factory to return our mock auth service
-      const originalGetInstance = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
+      setupMockAuthService(station, true, clearCacheMock)
 
-      try {
-        const response = await testableService.handleRequestClearCache(station)
+      const response = await testableService.handleRequestClearCache(station)
 
-        assert.strictEqual(clearCacheMock.mock.callCount(), 1)
-        assert.strictEqual(response.status, GenericStatus.Accepted)
-      } finally {
-        // Restore original factory method
-        Object.assign(OCPPAuthServiceFactory, { getInstance: originalGetInstance })
-      }
+      assert.strictEqual(clearCacheMock.mock.callCount(), 1)
+      assert.strictEqual(response.status, GenericStatus.Accepted)
     })
 
     await it('should not call idTagsCache.deleteIdTags() on ClearCache request', async () => {
@@ -114,133 +126,53 @@ await describe('C11 - Clear Authorization Data in Authorization Cache', async ()
   // CLR-002: Verify AuthCacheEnabled check per C11.FR.04
   await describe('CLR-002 - AuthCacheEnabled Check (C11.FR.04)', async () => {
     await it('should return Rejected when AuthCacheEnabled is false', async () => {
-      // Create a mock auth service with cache disabled
-      const mockAuthService = {
-        clearCache: (): void => {
-          throw new Error('clearCache should not be called when cache is disabled')
-        },
-        getConfiguration: () => ({
-          authorizationCacheEnabled: false,
-        }),
-      }
-
-      // Mock the factory to return our mock auth service
-      const originalGetInstance = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
+      setupMockAuthService(station, false, () => {
+        throw new Error('clearCache should not be called when cache is disabled')
       })
 
-      try {
-        const response = await testableService.handleRequestClearCache(station)
+      const response = await testableService.handleRequestClearCache(station)
 
-        assert.strictEqual(response.status, GenericStatus.Rejected)
-      } finally {
-        // Restore original factory method
-        Object.assign(OCPPAuthServiceFactory, { getInstance: originalGetInstance })
-      }
+      assert.strictEqual(response.status, GenericStatus.Rejected)
     })
 
     await it('should return Accepted when AuthCacheEnabled is true and clear succeeds', async () => {
-      // Create a mock auth service with cache enabled
-      const mockAuthService = {
-        clearCache: (): void => {
-          /* empty */
-        },
-        getConfiguration: () => ({
-          authorizationCacheEnabled: true,
-        }),
-      }
-
-      // Mock the factory to return our mock auth service
-      const originalGetInstance = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
+      setupMockAuthService(station, true)
 
-      try {
-        const response = await testableService.handleRequestClearCache(station)
+      const response = await testableService.handleRequestClearCache(station)
 
-        assert.strictEqual(response.status, GenericStatus.Accepted)
-      } finally {
-        // Restore original factory method
-        Object.assign(OCPPAuthServiceFactory, { getInstance: originalGetInstance })
-      }
+      assert.strictEqual(response.status, GenericStatus.Accepted)
     })
 
     await it('should return Rejected when clearCache throws an error', async () => {
-      // Create a mock auth service that throws on clearCache
-      const mockAuthService = {
-        clearCache: (): void => {
-          throw new Error('Cache clear failed')
-        },
-        getConfiguration: () => ({
-          authorizationCacheEnabled: true,
-        }),
-      }
-
-      // Mock the factory to return our mock auth service
-      const originalGetInstance = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
+      setupMockAuthService(station, true, () => {
+        throw new Error('Cache clear failed')
       })
 
-      try {
-        const response = await testableService.handleRequestClearCache(station)
+      const response = await testableService.handleRequestClearCache(station)
 
-        assert.strictEqual(response.status, GenericStatus.Rejected)
-      } finally {
-        // Restore original factory method
-        Object.assign(OCPPAuthServiceFactory, { getInstance: originalGetInstance })
-      }
+      assert.strictEqual(response.status, GenericStatus.Rejected)
     })
 
     await it('should not attempt to clear cache when AuthCacheEnabled is false', async () => {
       const clearCacheMock = mock.fn()
-      const mockAuthService = {
-        clearCache: clearCacheMock,
-        getConfiguration: () => ({
-          authorizationCacheEnabled: false,
-        }),
-      }
+      setupMockAuthService(station, false, clearCacheMock)
 
-      // Mock the factory to return our mock auth service
-      const originalGetInstance = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
-
-      try {
-        await testableService.handleRequestClearCache(station)
+      await testableService.handleRequestClearCache(station)
 
-        // clearCache should NOT be called when cache is disabled
-        assert.strictEqual(clearCacheMock.mock.callCount(), 0)
-      } finally {
-        // Restore original factory method
-        Object.assign(OCPPAuthServiceFactory, { getInstance: originalGetInstance })
-      }
+      assert.strictEqual(clearCacheMock.mock.callCount(), 0)
     })
   })
 
   // C11.FR.05: IF the CS does not support an Authorization Cache → Rejected
   await describe('C11.FR.05 - No Authorization Cache Support', async () => {
     await it('should return Rejected when authService factory fails (no cache support)', async () => {
-      // Mock factory to throw error (simulates no Authorization Cache support)
-      const originalGetInstance = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): never => {
-          throw new Error('Authorization Cache not supported')
-        },
-      })
-
-      try {
-        const response = await testableService.handleRequestClearCache(station)
+      const response = await withThrowingAuthServiceFactory(
+        'Authorization Cache not supported',
+        () => testableService.handleRequestClearCache(station)
+      )
 
-        // Per C11.FR.05: SHALL return Rejected if CS does not support Authorization Cache
-        assert.strictEqual(response.status, GenericStatus.Rejected)
-      } finally {
-        // Restore original factory method
-        Object.assign(OCPPAuthServiceFactory, { getInstance: originalGetInstance })
-      }
+      // Per C11.FR.05: SHALL return Rejected if CS does not support Authorization Cache
+      assert.strictEqual(response.status, GenericStatus.Rejected)
     })
   })
 })
index 4c10d69eabef36f0dcfb86d2d9db6763c88d60c1..bb95c5845283d7f219dbe7e46a7c74a897f18f31 100644 (file)
@@ -7,6 +7,7 @@ import assert from 'node:assert/strict'
 import { afterEach, beforeEach, describe, it } from 'node:test'
 
 import type { ChargingStation } from '../../../../src/charging-station/index.js'
+import type { LocalAuthListManager } from '../../../../src/charging-station/ocpp/auth/interfaces/OCPPAuthService.js'
 
 import { buildConfigKey } from '../../../../src/charging-station/index.js'
 import { createTestableIncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
@@ -29,12 +30,37 @@ import { Constants } from '../../../../src/utils/index.js'
 import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
 import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
 import { createMockChargingStation } from '../../helpers/StationHelpers.js'
+import {
+  createMockAuthService,
+  createTestAuthConfig,
+  injectMockAuthService,
+  withThrowingAuthServiceFactory,
+} from '../auth/helpers/MockFactories.js'
 import { upsertConfigurationKey } from './OCPP20TestUtils.js'
 
+/**
+ * Configure and inject a mock auth service for LocalAuthList request handling tests.
+ * @param station - Charging station under test
+ * @param manager - Local auth list manager exposed by the mock service
+ * @param localAuthListEnabled - Whether LocalAuthList is enabled in auth configuration
+ */
+function setupMockAuthService (
+  station: ChargingStation,
+  manager: LocalAuthListManager | undefined,
+  localAuthListEnabled = true
+): void {
+  injectMockAuthService(
+    station,
+    createMockAuthService({
+      getConfiguration: () => createTestAuthConfig({ localAuthListEnabled }),
+      getLocalAuthListManager: () => manager,
+    })
+  )
+}
+
 await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
   let station: ChargingStation
   let testableService: ReturnType<typeof createTestableIncomingRequestService>
-  let originalGetInstance: typeof OCPPAuthServiceFactory.getInstance
 
   /**
    * Toggle the LocalAuthListCtrlr.Enabled configuration key on the mock station.
@@ -62,12 +88,11 @@ await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
     station = mockStation
     const incomingRequestService = new OCPP20IncomingRequestService()
     testableService = createTestableIncomingRequestService(incomingRequestService)
-    originalGetInstance = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
     setLocalAuthListEnabled(true)
   })
 
   afterEach(() => {
-    Object.assign(OCPPAuthServiceFactory, { getInstance: originalGetInstance })
+    OCPPAuthServiceFactory.clearAllInstances()
     standardCleanup()
   })
 
@@ -78,13 +103,7 @@ await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
   await describe('GetLocalListVersion', async () => {
     await it('should return version 0 for empty list', () => {
       const manager = new InMemoryLocalAuthListManager()
-      const mockAuthService = {
-        getConfiguration: () => ({ localAuthListEnabled: true }),
-        getLocalAuthListManager: () => manager,
-      }
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
+      setupMockAuthService(station, manager)
 
       const response = testableService.handleRequestGetLocalListVersion(station)
 
@@ -93,13 +112,7 @@ await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
 
     await it('should return version 0 when local auth list disabled', () => {
       setLocalAuthListEnabled(false)
-      const mockAuthService = {
-        getConfiguration: () => ({ localAuthListEnabled: false }),
-        getLocalAuthListManager: () => undefined,
-      }
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
+      setupMockAuthService(station, undefined, false)
 
       const response = testableService.handleRequestGetLocalListVersion(station)
 
@@ -107,13 +120,7 @@ await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
     })
 
     await it('should return version 0 when manager is undefined', () => {
-      const mockAuthService = {
-        getConfiguration: () => ({ localAuthListEnabled: true }),
-        getLocalAuthListManager: () => undefined,
-      }
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
+      setupMockAuthService(station, undefined)
 
       const response = testableService.handleRequestGetLocalListVersion(station)
 
@@ -123,27 +130,17 @@ await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
     await it('should return correct version after SendLocalList', () => {
       const manager = new InMemoryLocalAuthListManager()
       manager.setEntries([{ identifier: 'TOKEN_001', status: 'Accepted' }], 5)
-      const mockAuthService = {
-        getConfiguration: () => ({ localAuthListEnabled: true }),
-        getLocalAuthListManager: () => manager,
-      }
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
+      setupMockAuthService(station, manager)
 
       const response = testableService.handleRequestGetLocalListVersion(station)
 
       assert.strictEqual(response.versionNumber, 5)
     })
 
-    await it('should return version 0 when auth service throws', () => {
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): never => {
-          throw new Error('Auth service unavailable')
-        },
-      })
-
-      const response = testableService.handleRequestGetLocalListVersion(station)
+    await it('should return version 0 when auth service throws', async () => {
+      const response = await withThrowingAuthServiceFactory('Auth service unavailable', () =>
+        testableService.handleRequestGetLocalListVersion(station)
+      )
 
       assert.strictEqual(response.versionNumber, 0)
     })
@@ -156,13 +153,7 @@ await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
   await describe('SendLocalList', async () => {
     await it('should accept Full update and replace list', () => {
       const manager = new InMemoryLocalAuthListManager()
-      const mockAuthService = {
-        getConfiguration: () => ({ localAuthListEnabled: true }),
-        getLocalAuthListManager: () => manager,
-      }
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
+      setupMockAuthService(station, manager)
 
       const response = testableService.handleRequestSendLocalList(station, {
         localAuthorizationList: [
@@ -191,13 +182,7 @@ await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
     await it('should accept Differential update with complex IdToken types', () => {
       const manager = new InMemoryLocalAuthListManager()
       manager.setEntries([{ identifier: 'EXISTING_001', status: 'Accepted' }], 1)
-      const mockAuthService = {
-        getConfiguration: () => ({ localAuthListEnabled: true }),
-        getLocalAuthListManager: () => manager,
-      }
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
+      setupMockAuthService(station, manager)
 
       const response = testableService.handleRequestSendLocalList(station, {
         localAuthorizationList: [
@@ -221,13 +206,7 @@ await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
 
     await it('should return Failed when disabled (with statusInfo)', () => {
       setLocalAuthListEnabled(false)
-      const mockAuthService = {
-        getConfiguration: () => ({ localAuthListEnabled: false }),
-        getLocalAuthListManager: () => undefined,
-      }
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
+      setupMockAuthService(station, undefined, false)
 
       const response = testableService.handleRequestSendLocalList(station, {
         localAuthorizationList: [],
@@ -241,13 +220,7 @@ await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
     })
 
     await it('should return Failed when manager is undefined', () => {
-      const mockAuthService = {
-        getConfiguration: () => ({ localAuthListEnabled: true }),
-        getLocalAuthListManager: () => undefined,
-      }
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
+      setupMockAuthService(station, undefined)
 
       const response = testableService.handleRequestSendLocalList(station, {
         localAuthorizationList: [],
@@ -268,13 +241,7 @@ await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
         ],
         1
       )
-      const mockAuthService = {
-        getConfiguration: () => ({ localAuthListEnabled: true }),
-        getLocalAuthListManager: () => manager,
-      }
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
+      setupMockAuthService(station, manager)
 
       const response = testableService.handleRequestSendLocalList(station, {
         localAuthorizationList: [],
@@ -291,18 +258,14 @@ await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
       assert.strictEqual(version, 2)
     })
 
-    await it('should return Failed when auth service throws', () => {
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): never => {
-          throw new Error('Auth service unavailable')
-        },
-      })
-
-      const response = testableService.handleRequestSendLocalList(station, {
-        localAuthorizationList: [],
-        updateType: OCPP20UpdateEnumType.Full,
-        versionNumber: 1,
-      })
+    await it('should return Failed when auth service throws', async () => {
+      const response = await withThrowingAuthServiceFactory('Auth service unavailable', () =>
+        testableService.handleRequestSendLocalList(station, {
+          localAuthorizationList: [],
+          updateType: OCPP20UpdateEnumType.Full,
+          versionNumber: 1,
+        })
+      )
 
       assert.strictEqual(response.status, OCPP20SendLocalListStatusEnumType.Failed)
     })
@@ -316,13 +279,7 @@ await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
         ],
         1
       )
-      const mockAuthService = {
-        getConfiguration: () => ({ localAuthListEnabled: true }),
-        getLocalAuthListManager: () => manager,
-      }
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
+      setupMockAuthService(station, manager)
 
       const response = testableService.handleRequestSendLocalList(station, {
         localAuthorizationList: [
@@ -347,13 +304,7 @@ await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
 
     await it('should preserve idTokenType metadata in entries', () => {
       const manager = new InMemoryLocalAuthListManager()
-      const mockAuthService = {
-        getConfiguration: () => ({ localAuthListEnabled: true }),
-        getLocalAuthListManager: () => manager,
-      }
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
+      setupMockAuthService(station, manager)
 
       testableService.handleRequestSendLocalList(station, {
         localAuthorizationList: [
@@ -375,13 +326,7 @@ await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
     await it('should handle Full update with undefined localAuthorizationList', () => {
       const manager = new InMemoryLocalAuthListManager()
       manager.setEntries([{ identifier: 'OLD_TOKEN', status: 'Accepted' }], 1)
-      const mockAuthService = {
-        getConfiguration: () => ({ localAuthListEnabled: true }),
-        getLocalAuthListManager: () => manager,
-      }
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
+      setupMockAuthService(station, manager)
 
       const response = testableService.handleRequestSendLocalList(station, {
         updateType: OCPP20UpdateEnumType.Full,
@@ -399,13 +344,7 @@ await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
 
     await it('should convert cacheExpiryDateTime to Date', () => {
       const manager = new InMemoryLocalAuthListManager()
-      const mockAuthService = {
-        getConfiguration: () => ({ localAuthListEnabled: true }),
-        getLocalAuthListManager: () => manager,
-      }
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
+      setupMockAuthService(station, manager)
 
       const expiryDate = new Date('2027-01-01T00:00:00.000Z')
 
@@ -431,13 +370,7 @@ await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
     await it('should return VersionMismatch for differential update with version < current', () => {
       const manager = new InMemoryLocalAuthListManager()
       manager.setEntries([{ identifier: 'TOKEN_001', status: 'Accepted' }], 5)
-      const mockAuthService = {
-        getConfiguration: () => ({ localAuthListEnabled: true }),
-        getLocalAuthListManager: () => manager,
-      }
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
+      setupMockAuthService(station, manager)
 
       const response = testableService.handleRequestSendLocalList(station, {
         localAuthorizationList: [
@@ -456,13 +389,7 @@ await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
     await it('should return VersionMismatch for differential update with version equal to current', () => {
       const manager = new InMemoryLocalAuthListManager()
       manager.setEntries([{ identifier: 'TOKEN_001', status: 'Accepted' }], 5)
-      const mockAuthService = {
-        getConfiguration: () => ({ localAuthListEnabled: true }),
-        getLocalAuthListManager: () => manager,
-      }
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
+      setupMockAuthService(station, manager)
 
       const response = testableService.handleRequestSendLocalList(station, {
         localAuthorizationList: [
@@ -480,13 +407,7 @@ await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
 
     await it('should return Failed with versionNumber <= 0', () => {
       const manager = new InMemoryLocalAuthListManager()
-      const mockAuthService = {
-        getConfiguration: () => ({ localAuthListEnabled: true }),
-        getLocalAuthListManager: () => manager,
-      }
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
+      setupMockAuthService(station, manager)
 
       const response = testableService.handleRequestSendLocalList(station, {
         localAuthorizationList: [],
@@ -499,13 +420,7 @@ await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
 
     await it('should return Failed with negative versionNumber', () => {
       const manager = new InMemoryLocalAuthListManager()
-      const mockAuthService = {
-        getConfiguration: () => ({ localAuthListEnabled: true }),
-        getLocalAuthListManager: () => manager,
-      }
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
+      setupMockAuthService(station, manager)
 
       const response = testableService.handleRequestSendLocalList(station, {
         localAuthorizationList: [],
@@ -519,13 +434,7 @@ await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
     await it('should accept Full update regardless of version (no VersionMismatch)', () => {
       const manager = new InMemoryLocalAuthListManager()
       manager.setEntries([{ identifier: 'TOKEN_001', status: 'Accepted' }], 5)
-      const mockAuthService = {
-        getConfiguration: () => ({ localAuthListEnabled: true }),
-        getLocalAuthListManager: () => manager,
-      }
-      Object.assign(OCPPAuthServiceFactory, {
-        getInstance: (): typeof mockAuthService => mockAuthService,
-      })
+      setupMockAuthService(station, manager)
 
       const response = testableService.handleRequestSendLocalList(station, {
         localAuthorizationList: [
index b3443ecc3c2d02012b14d2f32aba9d890b01a0c4..7d67f8be3bb9716d274ddd6f358dfcc22b882d4c 100644 (file)
@@ -22,19 +22,18 @@ import assert from 'node:assert/strict'
 import { afterEach, beforeEach, describe, it, mock } from 'node:test'
 
 import type { ChargingStation } from '../../../../src/charging-station/index.js'
-import type {
-  OCPP20TransactionEventRequest,
-  OCPP20TransactionEventResponse,
-  UUIDv4,
-} from '../../../../src/types/index.js'
+import type { OCPP20TransactionEventResponse } from '../../../../src/types/index.js'
 
+import {
+  createTestableResponseService,
+  type TestableOCPP20ResponseService,
+} from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
 import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js'
 import { OCPP20ServiceUtils } from '../../../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js'
 import {
   OCPP20AuthorizationStatusEnumType,
   OCPP20IdTokenEnumType,
   OCPP20TransactionEventEnumType,
-  OCPP20TriggerReasonEnumType,
   OCPPVersion,
 } from '../../../../src/types/index.js'
 import { Constants, logger } from '../../../../src/utils/index.js'
@@ -48,59 +47,7 @@ import {
   TEST_TRANSACTION_UUID,
 } from '../../ChargingStationTestConstants.js'
 import { createMockChargingStation } from '../../helpers/StationHelpers.js'
-
-interface TestableOCPP20ResponseService {
-  handleResponseTransactionEvent: (
-    chargingStation: ChargingStation,
-    payload: OCPP20TransactionEventResponse,
-    requestPayload: OCPP20TransactionEventRequest
-  ) => Promise<void>
-}
-
-/**
- * Builds a minimal OCPP20TransactionEventRequest with the given event type and
- * transaction id. Used as the request-payload twin in handler dispatch.
- * @param transactionId - The transaction UUID embedded in transactionInfo
- * @param eventType - The TransactionEvent type (Started/Updated/Ended)
- * @param idToken - Optional idToken to attach; required to exercise the auth-cache
- *   update path at OCPP20ResponseService.ts (C10.FR.01/04/05)
- * @param idToken.idToken - OCPP IdToken value (e.g. an RFID tag string)
- * @param idToken.type - OCPP 2.0.1 IdToken type (e.g. `ISO14443`, `ISO15693`, `Central`)
- * @returns A minimal OCPP20TransactionEventRequest
- */
-function buildTransactionEventRequest (
-  transactionId: UUIDv4,
-  eventType: OCPP20TransactionEventEnumType,
-  idToken?: { idToken: string; type: OCPP20IdTokenEnumType }
-): OCPP20TransactionEventRequest {
-  return {
-    eventType,
-    ...(idToken != null ? { idToken } : {}),
-    meterValue: [],
-    seqNo: 0,
-    timestamp: new Date(),
-    transactionInfo: {
-      transactionId,
-    },
-    triggerReason: OCPP20TriggerReasonEnumType.Authorized,
-  }
-}
-
-/**
- * Wraps an OCPP20ResponseService instance so its private
- * `handleResponseTransactionEvent` is reachable by tests via a typed cast.
- * Mirrors the helper in `OCPP20ResponseService-TransactionEvent.test.ts`.
- * @param service - The OCPP20ResponseService instance to wrap
- * @returns A typed interface exposing the private handler
- */
-function createTestableResponseService (
-  service: OCPP20ResponseService
-): TestableOCPP20ResponseService {
-  const serviceImpl = service as unknown as TestableOCPP20ResponseService
-  return {
-    handleResponseTransactionEvent: serviceImpl.handleResponseTransactionEvent.bind(service),
-  }
-}
+import { buildTransactionEventRequest } from './OCPP20ResponseServiceTestUtils.js'
 
 await describe('OCPP20ResponseService — forceTransactionOnInvalidIdToken (issue #1826)', async () => {
   let station: ChargingStation
index 0a7bff5141cdcc074cfe225578a88f7681bf400c..555745bd0bce21283b9a7a9529ff36de0bf67acf 100644 (file)
@@ -8,19 +8,17 @@ import assert from 'node:assert/strict'
 import { afterEach, beforeEach, describe, it, mock } from 'node:test'
 
 import type { ChargingStation } from '../../../../src/charging-station/index.js'
-import type {
-  OCPP20TransactionEventRequest,
-  OCPP20TransactionEventResponse,
-  UUIDv4,
-} from '../../../../src/types/index.js'
+import type { OCPP20TransactionEventResponse, UUIDv4 } from '../../../../src/types/index.js'
 
+import {
+  createTestableResponseService,
+  type TestableOCPP20ResponseService,
+} from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
 import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js'
 import { OCPP20ServiceUtils } from '../../../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js'
 import {
   OCPP20AuthorizationStatusEnumType,
   OCPP20MessageFormatEnumType,
-  OCPP20TransactionEventEnumType,
-  OCPP20TriggerReasonEnumType,
   OCPPVersion,
 } from '../../../../src/types/index.js'
 import { Constants } from '../../../../src/utils/index.js'
@@ -33,46 +31,7 @@ import {
   TEST_TRANSACTION_UUID,
 } from '../../ChargingStationTestConstants.js'
 import { createMockChargingStation } from '../../helpers/StationHelpers.js'
-
-interface TestableOCPP20ResponseService {
-  handleResponseTransactionEvent: (
-    chargingStation: ChargingStation,
-    payload: OCPP20TransactionEventResponse,
-    requestPayload: OCPP20TransactionEventRequest
-  ) => Promise<void>
-}
-
-/**
- * Builds a minimal OCPP20TransactionEventRequest for use as requestPayload in tests.
- * @param transactionId - The transaction UUID to embed in transactionInfo
- * @returns A minimal OCPP20TransactionEventRequest
- */
-function buildTransactionEventRequest (transactionId: UUIDv4): OCPP20TransactionEventRequest {
-  return {
-    eventType: OCPP20TransactionEventEnumType.Updated,
-    meterValue: [],
-    seqNo: 0,
-    timestamp: new Date(),
-    transactionInfo: {
-      transactionId,
-    },
-    triggerReason: OCPP20TriggerReasonEnumType.Authorized,
-  }
-}
-
-/**
- * Creates a testable wrapper around OCPP20ResponseService.
- * @param service - The OCPP20ResponseService instance to wrap
- * @returns A typed interface exposing private handler methods
- */
-function createTestableResponseService (
-  service: OCPP20ResponseService
-): TestableOCPP20ResponseService {
-  const serviceImpl = service as unknown as TestableOCPP20ResponseService
-  return {
-    handleResponseTransactionEvent: serviceImpl.handleResponseTransactionEvent.bind(service),
-  }
-}
+import { buildTransactionEventRequest } from './OCPP20ResponseServiceTestUtils.js'
 
 await describe('D01 - TransactionEvent Response', async () => {
   let station: ChargingStation
diff --git a/tests/charging-station/ocpp/2.0/OCPP20ResponseServiceTestUtils.ts b/tests/charging-station/ocpp/2.0/OCPP20ResponseServiceTestUtils.ts
new file mode 100644 (file)
index 0000000..aa6c9a4
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * @file OCPP20ResponseServiceTestUtils
+ * @description Test-only payload builders for OCPP 2.0 response-service tests
+ */
+import type {
+  OCPP20IdTokenType,
+  OCPP20TransactionEventRequest,
+  UUIDv4,
+} from '../../../../src/types/index.js'
+
+import {
+  OCPP20TransactionEventEnumType,
+  OCPP20TriggerReasonEnumType,
+} from '../../../../src/types/index.js'
+
+/**
+ * Build a minimal TransactionEvent request payload for response-service tests.
+ * @param transactionId - Transaction id to embed in the request payload.
+ * @param eventType - TransactionEvent type to use; defaults to Updated.
+ * @param idToken - Optional idToken used by authorization-cache test cases
+ *   (see `OCPP20IdTokenType` for field semantics).
+ * @returns Minimal TransactionEvent request payload.
+ */
+export const buildTransactionEventRequest = (
+  transactionId: UUIDv4,
+  eventType: OCPP20TransactionEventEnumType = OCPP20TransactionEventEnumType.Updated,
+  idToken?: OCPP20IdTokenType
+): OCPP20TransactionEventRequest => ({
+  eventType,
+  ...(idToken != null ? { idToken } : {}),
+  meterValue: [],
+  seqNo: 0,
+  timestamp: new Date(),
+  transactionInfo: {
+    transactionId,
+  },
+  triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+})
index 829c419af0be49817bce51a8e3169b8133d05dc4..dd712ad9196880152a1582278aec607c61a0aff1 100644 (file)
@@ -14,6 +14,7 @@ import type {
 import type { OCPPAuthServiceImpl } from '../../../../../src/charging-station/ocpp/auth/services/OCPPAuthServiceImpl.js'
 import type { LocalAuthStrategy } from '../../../../../src/charging-station/ocpp/auth/strategies/LocalAuthStrategy.js'
 
+import { OCPPAuthServiceFactory } from '../../../../../src/charging-station/ocpp/auth/services/OCPPAuthServiceFactory.js'
 import {
   type AuthConfiguration,
   AuthContext,
@@ -357,3 +358,53 @@ export const getTestAuthCache = (authService: OCPPAuthService): AuthCache => {
   assert.ok(cache != null, 'Auth cache must be available for test')
   return cache
 }
+
+// ============================================================================
+// Factory Injection Helpers
+// ============================================================================
+
+/**
+ * Inject a mock OCPPAuthService into the factory cache for the given station.
+ * Centralizes the station-id lookup and `setInstanceForTesting` plumbing used
+ * by OCPP 2.0 IncomingRequest tests (ClearCache, LocalAuthList, ...).
+ * @param station - Charging station under test (station-id read from stationInfo).
+ * @param service - Mock auth service to inject.
+ * @returns The injected service for chaining.
+ */
+export const injectMockAuthService = (
+  station: ChargingStation,
+  service: OCPPAuthService
+): OCPPAuthService => {
+  OCPPAuthServiceFactory.setInstanceForTesting(
+    station.stationInfo?.chargingStationId ?? 'unknown',
+    service
+  )
+  return service
+}
+
+/**
+ * Replace `OCPPAuthServiceFactory.getInstance` with a throwing function for the
+ * duration of `fn`, then restore the original. Used by tests that must exercise
+ * handler code paths surviving a factory failure (factory unavailable, no
+ * Authorization Cache support, ...). Required because `setInstanceForTesting`
+ * only injects a returned instance — it cannot make `getInstance` itself throw.
+ * @param errorMessage - Error message thrown by the patched `getInstance`.
+ * @param fn - Synchronous or asynchronous function executed while patched.
+ * @returns The value returned by `fn`.
+ */
+export const withThrowingAuthServiceFactory = async <T>(
+  errorMessage: string,
+  fn: () => Promise<T> | T
+): Promise<T> => {
+  const original = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
+  Object.assign(OCPPAuthServiceFactory, {
+    getInstance: (): never => {
+      throw new Error(errorMessage)
+    },
+  })
+  try {
+    return await fn()
+  } finally {
+    Object.assign(OCPPAuthServiceFactory, { getInstance: original })
+  }
+}
index 117189916b3b571de4003973d4b2e24d4cb00e0c..b528d38bf8afe01a8cfcd3e4b4bb8bad1025ab47 100644 (file)
@@ -6,7 +6,6 @@
 import type { IncomingMessage } from 'node:http'
 
 import assert from 'node:assert/strict'
-import { once } from 'node:events'
 import { afterEach, beforeEach, describe, it } from 'node:test'
 import { gunzipSync } from 'node:zlib'
 
@@ -18,6 +17,7 @@ import { ApplicationProtocol, ResponseStatus } from '../../../src/types/index.js
 import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
 import { TEST_UUID } from './UIServerTestConstants.js'
 import {
+  awaitFinish,
   createMockBootstrap,
   createMockIncomingMessage,
   createMockUIServerConfiguration,
@@ -35,6 +35,13 @@ class TestableUIHttpServer extends UIHttpServer {
     this.responseHandlers.set(uuid, res as never)
   }
 
+  public emitRequest (req: IncomingMessage, res: MockServerResponse): void {
+    const httpServer = Reflect.get(this, 'httpServer') as {
+      emit: (eventName: string, req: IncomingMessage, res: MockServerResponse) => boolean
+    }
+    httpServer.emit('request', req, res)
+  }
+
   public getAcceptsGzip (): Map<UUIDv4, boolean> {
     return Reflect.get(this, 'acceptsGzip')
   }
@@ -225,7 +232,7 @@ await describe('UIHttpServer', async () => {
 
     try {
       gatedServer.start()
-      httpServer.emit('request', req, res)
+      gatedServer.emitRequest(req, res)
     } finally {
       httpServer.removeAllListeners()
       gatedServer.stop()
@@ -269,7 +276,7 @@ await describe('UIHttpServer', async () => {
 
     try {
       gatedServer.start()
-      httpServer.emit('request', denyingReq, res)
+      gatedServer.emitRequest(denyingReq, res)
     } finally {
       httpServer.removeAllListeners()
       gatedServer.stop()
@@ -330,7 +337,7 @@ await describe('UIHttpServer', async () => {
       gzipServer.setAcceptsGzip(TEST_UUID, true)
       gzipServer.sendResponse([TEST_UUID, createLargePayload()])
 
-      await once(res, 'finish')
+      await awaitFinish(res)
 
       assert.strictEqual(res.headers['Content-Encoding'], 'gzip')
       assert.strictEqual(res.headers['Content-Type'], 'application/json')
@@ -345,7 +352,7 @@ await describe('UIHttpServer', async () => {
       gzipServer.setAcceptsGzip(TEST_UUID, true)
       gzipServer.sendResponse([TEST_UUID, payload])
 
-      await once(res, 'finish')
+      await awaitFinish(res)
 
       assert.notStrictEqual(res.bodyBuffer, undefined)
       if (res.bodyBuffer == null) {
@@ -376,7 +383,7 @@ await describe('UIHttpServer', async () => {
 
       gzipServer.sendResponse([TEST_UUID, createLargePayload()])
 
-      await once(res, 'finish')
+      await awaitFinish(res)
 
       assert.strictEqual(gzipServer.getAcceptsGzip().has(TEST_UUID), false)
     })
index 1297850a5d8efb87ef614a7518f984a0f5f75c26..66b5e0a6bf9d756386c2e9f4e7e0ce5c72da4fa3 100644 (file)
@@ -8,7 +8,6 @@ import type { mock } from 'node:test'
 import type { Registry } from 'prom-client'
 
 import assert from 'node:assert/strict'
-import { once } from 'node:events'
 import { afterEach, beforeEach, describe, it } from 'node:test'
 
 import type {
@@ -37,9 +36,11 @@ import {
 import { logger } from '../../../src/utils/index.js'
 import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
 import {
+  awaitFinish,
   createMockBootstrap,
   createMockIncomingMessage,
   createMockUIServerConfiguration,
+  drainResponses,
   MockServerResponse,
 } from './UIServerTestUtils.js'
 
@@ -184,7 +185,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
     server.start()
     const res = new MockServerResponse()
     server.emitRequest(buildMetricsRequest(), res)
-    await once(res, 'finish')
+    await awaitFinish(res)
     assert.strictEqual(res.statusCode, 200)
     assert.match(res.headers['Content-Type'] ?? '', /^text\/plain;\s*version=0\.0\.4/)
     assert.match(res.body ?? '', /^# HELP /m)
@@ -232,7 +233,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
     server.start()
     const res = new MockServerResponse()
     server.emitRequest(buildMetricsRequest(), res)
-    await once(res, 'finish')
+    await awaitFinish(res)
     const body = res.body ?? ''
     assert.match(body, /^simulator_charging_stations_configured_total\s+5$/m)
     assert.match(body, /^simulator_charging_stations_provisioned_total\s+2$/m)
@@ -248,7 +249,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
     server.start()
     const res = new MockServerResponse()
     server.emitRequest(buildMetricsRequest(), res)
-    await once(res, 'finish')
+    await awaitFinish(res)
     const body = res.body ?? ''
     assert.match(body, /simulator_station_started\{[^}]*hash_id="station-T5"[^}]*\}\s+1/)
     assert.match(body, /simulator_station_ws_state\{[^}]*hash_id="station-T5"[^}]*\}\s+1/)
@@ -262,7 +263,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
     server.start()
     const res = new MockServerResponse()
     server.emitRequest(buildMetricsRequest(), res)
-    await once(res, 'finish')
+    await awaitFinish(res)
     const body = res.body ?? ''
     const line = body
       .split('\n')
@@ -375,7 +376,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
         }),
         res
       )
-      await once(res, 'finish')
+      await awaitFinish(res)
       assert.strictEqual(res.statusCode, 200)
     } finally {
       authServer.stop()
@@ -422,7 +423,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
     server.start()
     const res = new MockServerResponse()
     server.emitRequest(buildMetricsRequest(), res)
-    await once(res, 'finish')
+    await awaitFinish(res)
     const body = res.body ?? ''
     for (const secret of [
       'SECRET-IDTAG-12345',
@@ -462,7 +463,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
     server.start()
     const res = new MockServerResponse()
     server.emitRequest(buildMetricsRequest(), res)
-    await once(res, 'finish')
+    await awaitFinish(res)
     const body = res.body ?? ''
     assert.ok(
       !/^fake_metric\b/m.test(body),
@@ -505,7 +506,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
     server.start()
     const res = new MockServerResponse()
     server.emitRequest(buildMetricsRequest(), res)
-    await once(res, 'finish')
+    await awaitFinish(res)
     assert.strictEqual(res.statusCode, 200)
     const matchingCalls = warnSpy.mock.calls.filter(call => {
       const message: unknown = call.arguments[0]
@@ -524,7 +525,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
     server.start()
     const res = new MockServerResponse()
     server.emitRequest(buildMetricsRequest(), res)
-    await once(res, 'finish')
+    await awaitFinish(res)
     const body = res.body ?? ''
     assert.ok(
       !/simulator_station_ws_state\{[^}]*hash_id="station-Mws"[^}]*\}/.test(body),
@@ -563,7 +564,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
     server.start()
     const res = new MockServerResponse()
     server.emitRequest(buildMetricsRequest(), res)
-    await once(res, 'finish')
+    await awaitFinish(res)
     const body = res.body ?? ''
     assert.match(body, /simulator_station_connectors_total\{[^}]*hash_id="station-T18"[^}]*\}\s+1/)
     const statusLine = body
@@ -595,7 +596,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
     probeServer.start()
     const probeRes = new MockServerResponse()
     probeServer.emitRequest(buildMetricsRequest(), probeRes)
-    await once(probeRes, 'finish')
+    await awaitFinish(probeRes)
     const probedSampleCount = (probeRes.body ?? '')
       .split('\n')
       .filter(line => line.length > 0 && !line.startsWith('#')).length
@@ -616,7 +617,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
     exactServer.start()
     const exactRes = new MockServerResponse()
     exactServer.emitRequest(buildMetricsRequest(), exactRes)
-    await once(exactRes, 'finish')
+    await awaitFinish(exactRes)
     const exactSoftCapCalls = warnSpy.mock.calls.filter(call => {
       const message: unknown = call.arguments[0]
       return typeof message === 'string' && message.includes(METRICS_SOFT_CAP_WARN_PREFIX)
@@ -644,7 +645,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
     belowServer.start()
     const belowRes = new MockServerResponse()
     belowServer.emitRequest(buildMetricsRequest(), belowRes)
-    await once(belowRes, 'finish')
+    await awaitFinish(belowRes)
     const belowSoftCapCalls = warnSpy.mock.calls.filter(call => {
       const message: unknown = call.arguments[0]
       return typeof message === 'string' && message.includes(METRICS_SOFT_CAP_WARN_PREFIX)
@@ -676,7 +677,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
     probeServer.start()
     const probeRes = new MockServerResponse()
     probeServer.emitRequest(buildMetricsRequest(), probeRes)
-    await once(probeRes, 'finish')
+    await awaitFinish(probeRes)
     const probedSampleCount = (probeRes.body ?? '')
       .split('\n')
       .filter(line => line.length > 0 && !line.startsWith('#')).length
@@ -699,7 +700,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
     const resB = new MockServerResponse()
     concurrentServer.emitRequest(buildMetricsRequest(), resA)
     concurrentServer.emitRequest(buildMetricsRequest(), resB)
-    await Promise.all([once(resA, 'finish'), once(resB, 'finish')])
+    await drainResponses([resA, resB])
     concurrentServer.stop()
 
     assert.strictEqual(resA.statusCode, 200)
@@ -842,7 +843,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
       server.start()
       const res = new MockServerResponse()
       server.emitRequest(buildMetricsRequest(), res)
-      await once(res, 'finish')
+      await awaitFinish(res)
       const body = res.body ?? ''
       assert.ok(body.includes('1.0.0-✓'), 'non-ASCII version must propagate into exposition body')
       const checkMarkOccurrences = (body.match(/✓/gu) ?? []).length
@@ -1345,7 +1346,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
       const chain0 = getChain()
       const r1 = new MockServerResponse()
       server.emitRequest(buildMetricsRequest(), r1)
-      await once(r1, 'finish')
+      await awaitFinish(r1)
       const chainAfterScrape1 = getChain()
       assert.notStrictEqual(
         chainAfterScrape1,
@@ -1368,7 +1369,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
       )
       const r2 = new MockServerResponse()
       server.emitRequest(buildMetricsRequest(), r2)
-      await once(r2, 'finish')
+      await awaitFinish(r2)
       const chainAfterScrape2 = getChain()
       const all = [chain0, chainAfterScrape1, chainAfterStop, chain1, chainAfterScrape2]
       assert.strictEqual(
@@ -1390,7 +1391,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
       localServer.start()
       const r1 = new MockServerResponse()
       localServer.emitRequest(buildMetricsRequest(), r1)
-      await once(r1, 'finish')
+      await awaitFinish(r1)
       const registry = localServer.getMetricsRegistry()
       assert.ok(registry !== undefined, 'precondition: registry present before stop()')
       let clearCalls = 0
@@ -1441,7 +1442,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
       }
       const res = new MockServerResponse()
       localServer.emitRequest(buildMetricsRequest(), res)
-      await once(res, 'finish')
+      await awaitFinish(res)
       const rejectedChain = Reflect.get(localServer, 'metricsScrapeChain') as Promise<void>
       let rejected = false
       await rejectedChain.catch(() => {
@@ -1495,7 +1496,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
         setImmediate(resolve)
       })
       releaseGate()
-      await once(res, 'finish')
+      await awaitFinish(res)
       const scrapeWarns = warnSpy.mock.calls.filter(
         c =>
           typeof c.arguments[0] === 'string' &&
@@ -1524,7 +1525,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
       localServer.start()
       const res = new MockServerResponse()
       localServer.emitRequest(buildMetricsRequest(), res)
-      await once(res, 'finish')
+      await awaitFinish(res)
       assert.strictEqual(res.statusCode, 200)
       const sampleCount = (res.body ?? '')
         .split('\n')
@@ -1568,7 +1569,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
       localServer.start()
       const res = new MockServerResponse()
       localServer.emitRequest(buildMetricsRequest(), res)
-      await once(res, 'finish')
+      await awaitFinish(res)
       assert.strictEqual(res.statusCode, 200)
       const scrapeWarns = warnSpy.mock.calls.filter(
         c =>
@@ -1616,7 +1617,7 @@ await describe('UIHttpServer /metrics endpoint (issue #851)', async () => {
         'precondition: stop() nulled this.metricsRegistry mid-flight'
       )
       releaseGate()
-      await once(res, 'finish')
+      await awaitFinish(res)
       assert.strictEqual(
         res.statusCode,
         200,