From c2ea863e0d006a9053bae2375febd1e2f2717176 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Mon, 29 Jun 2026 20:24:09 +0200 Subject: [PATCH] test: use shared response test helpers (#1930) MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit * test(ui-server): use response drain helpers Signed-off-by: Jérôme Benoit * test(ocpp): share response service test helpers Signed-off-by: Jérôme Benoit * test(ocpp): use auth factory test instances Signed-off-by: Jérôme Benoit * 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 * 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 * 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 * 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 --------- Signed-off-by: Jérôme Benoit --- .../OCPP20ResponseServiceTestable.ts | 63 ++++++ .../ocpp/2.0/__testable__/index.ts | 5 + ...0IncomingRequestService-ClearCache.test.ts | 182 +++++---------- ...comingRequestService-LocalAuthList.test.ts | 207 +++++------------- ...20ResponseService-ForceTxOnInvalid.test.ts | 65 +----- ...20ResponseService-TransactionEvent.test.ts | 53 +---- .../2.0/OCPP20ResponseServiceTestUtils.ts | 38 ++++ .../ocpp/auth/helpers/MockFactories.ts | 51 +++++ .../ui-server/UIHttpServer.test.ts | 19 +- .../ui-server/UIMetricsEndpoint.test.ts | 51 ++--- 10 files changed, 323 insertions(+), 411 deletions(-) create mode 100644 src/charging-station/ocpp/2.0/__testable__/OCPP20ResponseServiceTestable.ts create mode 100644 tests/charging-station/ocpp/2.0/OCPP20ResponseServiceTestUtils.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 index 00000000..36d7baa3 --- /dev/null +++ b/src/charging-station/ocpp/2.0/__testable__/OCPP20ResponseServiceTestable.ts @@ -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 +} + +/** + * 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), + } +} diff --git a/src/charging-station/ocpp/2.0/__testable__/index.ts b/src/charging-station/ocpp/2.0/__testable__/index.ts index c3bd89cb..40629a6f 100644 --- a/src/charging-station/ocpp/2.0/__testable__/index.ts +++ b/src/charging-station/ocpp/2.0/__testable__/index.ts @@ -320,6 +320,11 @@ export { type TestableRequestServiceResult, } from './OCPP20RequestServiceTestable.js' +export { + createTestableResponseService, + type TestableOCPP20ResponseService, +} from './OCPP20ResponseServiceTestable.js' + export { createTestableVariableManager, type TestableOCPP20VariableManager, diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ClearCache.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ClearCache.test.ts index 2c77a527..13fe1d6b 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ClearCache.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ClearCache.test.ts @@ -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 @@ -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) }) }) }) diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-LocalAuthList.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-LocalAuthList.test.ts index 4c10d69e..bb95c584 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-LocalAuthList.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-LocalAuthList.test.ts @@ -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 - 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: [ diff --git a/tests/charging-station/ocpp/2.0/OCPP20ResponseService-ForceTxOnInvalid.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ResponseService-ForceTxOnInvalid.test.ts index b3443ecc..7d67f8be 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20ResponseService-ForceTxOnInvalid.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20ResponseService-ForceTxOnInvalid.test.ts @@ -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 -} - -/** - * 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 diff --git a/tests/charging-station/ocpp/2.0/OCPP20ResponseService-TransactionEvent.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ResponseService-TransactionEvent.test.ts index 0a7bff51..555745bd 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20ResponseService-TransactionEvent.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20ResponseService-TransactionEvent.test.ts @@ -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 -} - -/** - * 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 index 00000000..aa6c9a45 --- /dev/null +++ b/tests/charging-station/ocpp/2.0/OCPP20ResponseServiceTestUtils.ts @@ -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, +}) diff --git a/tests/charging-station/ocpp/auth/helpers/MockFactories.ts b/tests/charging-station/ocpp/auth/helpers/MockFactories.ts index 829c419a..dd712ad9 100644 --- a/tests/charging-station/ocpp/auth/helpers/MockFactories.ts +++ b/tests/charging-station/ocpp/auth/helpers/MockFactories.ts @@ -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 ( + errorMessage: string, + fn: () => Promise | T +): Promise => { + 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 }) + } +} diff --git a/tests/charging-station/ui-server/UIHttpServer.test.ts b/tests/charging-station/ui-server/UIHttpServer.test.ts index 11718991..b528d38b 100644 --- a/tests/charging-station/ui-server/UIHttpServer.test.ts +++ b/tests/charging-station/ui-server/UIHttpServer.test.ts @@ -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 { 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) }) diff --git a/tests/charging-station/ui-server/UIMetricsEndpoint.test.ts b/tests/charging-station/ui-server/UIMetricsEndpoint.test.ts index 1297850a..66b5e0a6 100644 --- a/tests/charging-station/ui-server/UIMetricsEndpoint.test.ts +++ b/tests/charging-station/ui-server/UIMetricsEndpoint.test.ts @@ -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 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, -- 2.53.0